summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ironic_python_agent/cmd/agent.py3
-rw-r--r--ironic_python_agent/extensions/standby.py72
-rwxr-xr-xironic_python_agent/shell/copy_configdrive_to_disk.sh25
-rw-r--r--ironic_python_agent/tests/extensions/standby.py124
4 files changed, 113 insertions, 111 deletions
diff --git a/ironic_python_agent/cmd/agent.py b/ironic_python_agent/cmd/agent.py
index 31390378..770d6f17 100644
--- a/ironic_python_agent/cmd/agent.py
+++ b/ironic_python_agent/cmd/agent.py
@@ -18,7 +18,8 @@ from ironic_python_agent import agent
from ironic_python_agent.openstack.common import log
-LOG = log.getLogger()
+log.setup('ironic-python-agent')
+LOG = log.getLogger(__name__)
def _get_kernel_params():
diff --git a/ironic_python_agent/extensions/standby.py b/ironic_python_agent/extensions/standby.py
index c2c0b8ab..17c031c7 100644
--- a/ironic_python_agent/extensions/standby.py
+++ b/ironic_python_agent/extensions/standby.py
@@ -12,17 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import base64
+import gzip
import hashlib
import os
import requests
-import subprocess
+import six
+import StringIO
import time
-from ironic_python_agent import configdrive
from ironic_python_agent import errors
from ironic_python_agent.extensions import base
from ironic_python_agent import hardware
from ironic_python_agent.openstack.common import log
+from ironic_python_agent import utils
LOG = log.getLogger(__name__)
@@ -47,7 +50,7 @@ def _write_image(image_info, device):
script = _path_to_script('shell/write_image.sh')
command = ['/bin/bash', script, image, device]
LOG.info('Writing image with command: {0}'.format(' '.join(command)))
- exit_code = subprocess.call(command)
+ exit_code = utils.execute(*command)
if exit_code != 0:
raise errors.ImageWriteError(exit_code, device)
totaltime = time.time() - starttime
@@ -55,20 +58,33 @@ def _write_image(image_info, device):
image, device, totaltime))
-def _copy_configdrive_to_disk(configdrive_dir, device):
+def _write_configdrive_to_file(configdrive, filename):
+ LOG.debug('Writing configdrive to {0}'.format(filename))
+ # configdrive data is base64'd, decode it first
+ data = StringIO.StringIO(base64.b64decode(configdrive))
+ gunzipped = gzip.GzipFile('configdrive', 'rb', 9, data)
+ with open(filename, 'wb') as f:
+ f.write(gunzipped.read())
+ gunzipped.close()
+
+
+def _write_configdrive_to_partition(configdrive, device):
+ filename = _configdrive_location()
+ _write_configdrive_to_file(configdrive, filename)
+
starttime = time.time()
script = _path_to_script('shell/copy_configdrive_to_disk.sh')
- command = ['/bin/bash', script, configdrive_dir, device]
+ command = ['/bin/bash', script, filename, device]
LOG.info('copying configdrive to disk with command {0}'.format(
' '.join(command)))
- exit_code = subprocess.call(command)
+ exit_code = utils.execute(*command)
if exit_code != 0:
raise errors.ConfigDriveWriteError(exit_code, device)
totaltime = time.time() - starttime
LOG.info('configdrive copied from {0} to {1} in {2} seconds'.format(
- configdrive_dir,
+ configdrive,
device,
totaltime))
@@ -114,28 +130,22 @@ def _download_image(image_info):
def _verify_image(image_info, image_location):
- hashes = image_info['hashes']
- for k, v in hashes.items():
- algo = getattr(hashlib, k, None)
- if algo is None:
- continue
- log_msg = 'Verifying image at {0} with algorithm {1} against hash {2}'
- LOG.debug(log_msg.format(image_location, k, v))
- hash_ = algo(open(image_location).read()).hexdigest()
- if hash_ == v:
- return True
- else:
- log_msg = ('Image verification failed. Location: {0};'
- 'algorithm: {1}; image hash: {2};'
- 'verification hash: {3}')
- LOG.warning(log_msg.format(image_location, k, hash_, v))
+ checksum = image_info['checksum']
+ log_msg = 'Verifying image at {0} against MD5 checksum {1}'
+ LOG.debug(log_msg.format(image_location, checksum))
+ hash_ = hashlib.md5(open(image_location).read()).hexdigest()
+ if hash_ == checksum:
+ return True
+ log_msg = ('Image verification failed. Location: {0};'
+ 'image hash: {1}; verification hash: {2}')
+ LOG.warning(log_msg.format(image_location, checksum, hash_))
return False
def _validate_image_info(ext, image_info=None, **kwargs):
image_info = image_info or {}
- for field in ['id', 'urls', 'hashes']:
+ for field in ['id', 'urls', 'checksum']:
if field not in image_info:
msg = 'Image is missing \'{0}\' field.'.format(field)
raise errors.InvalidCommandParamsError(msg)
@@ -144,10 +154,10 @@ def _validate_image_info(ext, image_info=None, **kwargs):
raise errors.InvalidCommandParamsError(
'Image \'urls\' must be a list with at least one element.')
- if type(image_info['hashes']) != dict or not image_info['hashes']:
+ if (not isinstance(image_info['checksum'], six.string_types)
+ or not image_info['checksum']):
raise errors.InvalidCommandParamsError(
- 'Image \'hashes\' must be a dictionary with at least one '
- 'element.')
+ 'Image \'checksum\' must be a non-empty string.')
class StandbyExtension(base.BaseAgentExtension):
@@ -171,9 +181,7 @@ class StandbyExtension(base.BaseAgentExtension):
@base.async_command(_validate_image_info)
def prepare_image(self,
image_info=None,
- metadata=None,
- files=None):
- location = _configdrive_location()
+ configdrive=None):
device = hardware.get_manager().get_os_install_device()
# don't write image again if already cached
@@ -182,9 +190,7 @@ class StandbyExtension(base.BaseAgentExtension):
_write_image(image_info, device)
self.cached_image_id = image_info['id']
- LOG.debug('Writing configdrive to {0}'.format(location))
- configdrive.write_configdrive(location, metadata, files)
- _copy_configdrive_to_disk(location, device)
+ _write_configdrive_to_partition(configdrive, device)
@base.async_command()
def run_image(self):
@@ -192,6 +198,6 @@ class StandbyExtension(base.BaseAgentExtension):
LOG.info('Rebooting system')
command = ['/bin/bash', script]
# this should never return if successful
- exit_code = subprocess.call(command)
+ exit_code = utils.execute(*command)
if exit_code != 0:
raise errors.SystemRebootError(exit_code)
diff --git a/ironic_python_agent/shell/copy_configdrive_to_disk.sh b/ironic_python_agent/shell/copy_configdrive_to_disk.sh
index e3f08d40..768a37da 100755
--- a/ironic_python_agent/shell/copy_configdrive_to_disk.sh
+++ b/ironic_python_agent/shell/copy_configdrive_to_disk.sh
@@ -22,39 +22,28 @@ log() {
usage() {
[[ -z "$1" ]] || echo -e "USAGE ERROR: $@\n"
- echo "`basename $0`: CONFIGDRIVE_DIR DEVICE"
- echo " - This script injects CONFIGDRIVE_DIR contents as an iso9660"
+ echo "`basename $0`: CONFIGDRIVE DEVICE"
+ echo " - This script injects CONFIGDRIVE contents as an iso9660"
echo " filesystem on a partition at the end of DEVICE."
exit 1
}
-CONFIGDRIVE_DIR="$1"
+CONFIGDRIVE="$1"
DEVICE="$2"
-[[ -d $CONFIGDRIVE_DIR ]] || usage "$CONFIGDRIVE_DIR (CONFIGDRIVE_DIR) is not a directory"
+[[ -f $CONFIGDRIVE ]] || usage "$CONFIGDRIVE (CONFIGDRIVE) is not a regular file"
[[ -b $DEVICE ]] || usage "$DEVICE (DEVICE) is not a block device"
# Create small partition at the end of the device
log "Adding configdrive partition to $DEVICE"
-parted -a optimal -s -- $DEVICE mkpart primary ext2 -16MiB -0
+parted -a optimal -s -- $DEVICE mkpart primary ext2 -64MiB -0
# Find partition we just created
# Dump all partitions, ignore empty ones, then get the last partition ID
ISO_PARTITION=`sfdisk --dump $DEVICE | grep -v ' 0,' | tail -n1 | awk '{print $1}'`
# This generates the ISO image of the config drive.
-log "Writing Configdrive contents in $CONFIGDRIVE_DIR to $ISO_PARTITION"
-genisoimage \
- -o ${ISO_PARTITION} \
- -ldots \
- -input-charset 'utf-8' \
- -allow-lowercase \
- -allow-multidot \
- -l \
- -publisher "ironic" \
- -J \
- -r \
- -V 'config-2' \
- ${CONFIGDRIVE_DIR}
+log "Writing Configdrive contents in $CONFIGDRIVE to $ISO_PARTITION"
+dd if=$CONFIGDRIVE of=$ISO_PARTITION bs=64K oflag=direct
log "${DEVICE} imaged successfully!"
diff --git a/ironic_python_agent/tests/extensions/standby.py b/ironic_python_agent/tests/extensions/standby.py
index e65ac2a9..a1449d0d 100644
--- a/ironic_python_agent/tests/extensions/standby.py
+++ b/ironic_python_agent/tests/extensions/standby.py
@@ -36,16 +36,14 @@ class TestStandbyExtension(test_base.BaseTestCase):
'urls': [
'http://example.org',
],
- 'hashes': {
- 'md5': 'abc123',
- },
+ 'checksum': 'abc123'
}
def test_validate_image_info_success(self):
standby._validate_image_info(None, self._build_fake_image_info())
def test_validate_image_info_missing_field(self):
- for field in ['id', 'urls', 'hashes']:
+ for field in ['id', 'urls', 'checksum']:
invalid_info = self._build_fake_image_info()
del invalid_info[field]
@@ -69,17 +67,17 @@ class TestStandbyExtension(test_base.BaseTestCase):
standby._validate_image_info,
invalid_info)
- def test_validate_image_info_invalid_hashes(self):
+ def test_validate_image_info_invalid_checksum(self):
invalid_info = self._build_fake_image_info()
- invalid_info['hashes'] = 'this_is_not_a_dict'
+ invalid_info['checksum'] = {'not': 'a string'}
self.assertRaises(errors.InvalidCommandParamsError,
standby._validate_image_info,
invalid_info)
- def test_validate_image_info_empty_hashes(self):
+ def test_validate_image_info_empty_checksum(self):
invalid_info = self._build_fake_image_info()
- invalid_info['hashes'] = {}
+ invalid_info['checksum'] = ''
self.assertRaises(errors.InvalidCommandParamsError,
standby._validate_image_info,
@@ -103,49 +101,70 @@ class TestStandbyExtension(test_base.BaseTestCase):
self.assertEqual(location, '/tmp/fake_id')
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
- @mock.patch('subprocess.call', autospec=True)
- def test_write_image(self, call_mock, open_mock):
+ @mock.patch('ironic_python_agent.utils.execute', autospec=True)
+ def test_write_image(self, execute_mock, open_mock):
image_info = self._build_fake_image_info()
device = '/dev/sda'
location = standby._image_location(image_info)
script = standby._path_to_script('shell/write_image.sh')
command = ['/bin/bash', script, location, device]
- call_mock.return_value = 0
+ execute_mock.return_value = 0
standby._write_image(image_info, device)
- call_mock.assert_called_once_with(command)
+ execute_mock.assert_called_once_with(*command)
- call_mock.reset_mock()
- call_mock.return_value = 1
+ execute_mock.reset_mock()
+ execute_mock.return_value = 1
self.assertRaises(errors.ImageWriteError,
standby._write_image,
image_info,
device)
- call_mock.assert_called_once_with(command)
+ execute_mock.assert_called_once_with(*command)
+ @mock.patch('gzip.GzipFile', autospec=True)
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
- @mock.patch('subprocess.call', autospec=True)
- def test_copy_configdrive_to_disk(self, call_mock, open_mock):
+ @mock.patch('base64.b64decode', autospec=True)
+ def test_write_configdrive_to_file(self, b64_mock, open_mock, gzip_mock):
+ open_mock.return_value.__enter__ = lambda s: s
+ open_mock.return_value.__exit__ = mock.Mock()
+ write_mock = open_mock.return_value.write
+ gzip_read_mock = gzip_mock.return_value.read
+ gzip_read_mock.return_value = 'ungzipped'
+ b64_mock.return_value = 'configdrive_data'
+ filename = standby._configdrive_location()
+
+ standby._write_configdrive_to_file('b64data', filename)
+ open_mock.assert_called_once_with(filename, 'wb')
+ gzip_read_mock.assert_called_once_with()
+ write_mock.assert_called_once_with('ungzipped')
+
+ @mock.patch(('ironic_python_agent.extensions.standby.'
+ '_write_configdrive_to_file'),
+ autospec=True)
+ @mock.patch(OPEN_FUNCTION_NAME, autospec=True)
+ @mock.patch('ironic_python_agent.utils.execute', autospec=True)
+ def test_write_configdrive_to_partition(self, execute_mock, open_mock,
+ configdrive_mock):
device = '/dev/sda'
- configdrive = 'configdrive'
+ configdrive = standby._configdrive_location()
script = standby._path_to_script('shell/copy_configdrive_to_disk.sh')
command = ['/bin/bash', script, configdrive, device]
- call_mock.return_value = 0
+ execute_mock.return_value = 0
- standby._copy_configdrive_to_disk(configdrive, device)
- call_mock.assert_called_once_with(command)
+ standby._write_configdrive_to_partition(configdrive, device)
+ execute_mock.assert_called_once_with(*command)
- call_mock.reset_mock()
- call_mock.return_value = 1
+ execute_mock.reset_mock()
+ execute_mock.return_value = 1
self.assertRaises(errors.ConfigDriveWriteError,
- standby._copy_configdrive_to_disk,
+ standby._write_configdrive_to_partition,
configdrive,
device)
- call_mock.assert_called_once_with(command)
+ execute_mock.assert_called_once_with(*command)
@mock.patch('hashlib.md5', autospec=True)
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
@@ -160,7 +179,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
read_mock = open_mock.return_value.read
read_mock.return_value = 'content'
hexdigest_mock = md5_mock.return_value.hexdigest
- hexdigest_mock.return_value = list(image_info['hashes'].values())[0]
+ hexdigest_mock.return_value = image_info['checksum']
standby._download_image(image_info)
requests_mock.assert_called_once_with(image_info['urls'][0],
@@ -194,21 +213,16 @@ class TestStandbyExtension(test_base.BaseTestCase):
image_info)
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
- @mock.patch('hashlib.sha1', autospec=True)
@mock.patch('hashlib.md5', autospec=True)
- def test_verify_image_success(self, md5_mock, sha1_mock, open_mock):
+ def test_verify_image_success(self, md5_mock, open_mock):
image_info = self._build_fake_image_info()
- image_info['hashes']['sha1'] = image_info['hashes']['md5']
hexdigest_mock = md5_mock.return_value.hexdigest
- hexdigest_mock.return_value = image_info['hashes']['md5']
- hexdigest_mock = sha1_mock.return_value.hexdigest
- hexdigest_mock.return_value = image_info['hashes']['sha1']
+ hexdigest_mock.return_value = image_info['checksum']
image_location = '/foo/bar'
verified = standby._verify_image(image_info, image_location)
self.assertTrue(verified)
- # make sure we only check one hash, even though both are valid
- self.assertEqual(md5_mock.call_count + sha1_mock.call_count, 1)
+ self.assertEqual(1, md5_mock.call_count)
@mock.patch(OPEN_FUNCTION_NAME, autospec=True)
@mock.patch('hashlib.md5', autospec=True)
@@ -243,10 +257,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
self.assertEqual(None, async_result.command_result)
@mock.patch(('ironic_python_agent.extensions.standby.'
- '_copy_configdrive_to_disk'),
- autospec=True)
- @mock.patch(('ironic_python_agent.extensions.standby.configdrive.'
- 'write_configdrive'),
+ '_write_configdrive_to_partition'),
autospec=True)
@mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
@mock.patch('ironic_python_agent.extensions.standby._write_image',
@@ -260,65 +271,60 @@ class TestStandbyExtension(test_base.BaseTestCase):
download_mock,
write_mock,
hardware_mock,
- configdrive_mock,
configdrive_copy_mock):
image_info = self._build_fake_image_info()
- location_mock.return_value = 'THE CLOUD'
+ location_mock.return_value = '/tmp/configdrive'
download_mock.return_value = None
write_mock.return_value = None
manager_mock = hardware_mock.return_value
manager_mock.get_os_install_device.return_value = 'manager'
- configdrive_mock.return_value = None
configdrive_copy_mock.return_value = None
async_result = self.agent_extension.prepare_image('prepare_image',
- image_info=image_info,
- metadata={},
- files=[])
+ image_info=image_info,
+ configdrive='configdrive_data')
async_result.join()
download_mock.assert_called_once_with(image_info)
write_mock.assert_called_once_with(image_info, 'manager')
- configdrive_mock.assert_called_once_with('THE CLOUD', {}, [])
- configdrive_copy_mock.assert_called_once_with('THE CLOUD', 'manager')
+ configdrive_copy_mock.assert_called_once_with('configdrive_data',
+ 'manager')
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual(None, async_result.command_result)
download_mock.reset_mock()
write_mock.reset_mock()
- configdrive_mock.reset_mock()
configdrive_copy_mock.reset_mock()
# image is now cached, make sure download/write doesn't happen
async_result = self.agent_extension.prepare_image('prepare_image',
- image_info=image_info,
- metadata={},
- files=[])
+ image_info=image_info,
+ configdrive='configdrive_data')
async_result.join()
self.assertEqual(download_mock.call_count, 0)
self.assertEqual(write_mock.call_count, 0)
- configdrive_mock.assert_called_once_with('THE CLOUD', {}, [])
- configdrive_copy_mock.assert_called_once_with('THE CLOUD', 'manager')
+ configdrive_copy_mock.assert_called_once_with('configdrive_data',
+ 'manager')
self.assertEqual('SUCCEEDED', async_result.command_status)
self.assertEqual(None, async_result.command_result)
- @mock.patch('subprocess.call', autospec=True)
- def test_run_image(self, call_mock):
+ @mock.patch('ironic_python_agent.utils.execute', autospec=True)
+ def test_run_image(self, execute_mock):
script = standby._path_to_script('shell/reboot.sh')
command = ['/bin/bash', script]
- call_mock.return_value = 0
+ execute_mock.return_value = 0
success_result = self.agent_extension.run_image('run_image')
success_result.join()
- call_mock.assert_called_once_with(command)
+ execute_mock.assert_called_once_with(*command)
- call_mock.reset_mock()
- call_mock.return_value = 1
+ execute_mock.reset_mock()
+ execute_mock.return_value = 1
failed_result = self.agent_extension.run_image('run_image')
failed_result.join()
- call_mock.assert_called_once_with(command)
+ execute_mock.assert_called_once_with(*command)
self.assertEqual('FAILED', failed_result.command_status)