Add docstrings to all functions in Agent standby extension
Change-Id: Ic8101a6b29dee4b79c2d7f3dc064e4c98a9a0741 Partial-Bug: 1367915
This commit is contained in:
parent
698211b771
commit
3cf5369cb6
|
@ -35,20 +35,44 @@ IMAGE_CHUNK_SIZE = 1024 * 1024 # 1MB
|
|||
|
||||
|
||||
def _configdrive_location():
|
||||
"""Get the configdrive location in the local file system.
|
||||
|
||||
:returns: The full, absolute path to the configdrive as a string.
|
||||
"""
|
||||
return '/tmp/configdrive'
|
||||
|
||||
|
||||
def _image_location(image_info):
|
||||
"""Get the location of the image in the local file system.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:returns: The full, absolute path to the image as a string.
|
||||
"""
|
||||
return '/tmp/{0}'.format(image_info['id'])
|
||||
|
||||
|
||||
def _path_to_script(script):
|
||||
"""Get the location of a script which ships with ironic-python-agent.
|
||||
|
||||
:param script: The script name as a string.
|
||||
:returns: The relative path to the script.
|
||||
"""
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
return os.path.join(cwd, '..', script)
|
||||
|
||||
|
||||
def _write_partition_image(image, image_info, device):
|
||||
"""Call disk_util to create partition and write the partition image."""
|
||||
"""Call disk_util to create partition and write the partition image.
|
||||
|
||||
:param image: Local path to image file to be written to the partition.
|
||||
:param image_info: Image information dictionary.
|
||||
:param device: The device name, as a string, on which to store the image.
|
||||
Example: '/dev/sda'
|
||||
|
||||
:raises: InvalidCommandParamsError if the partition is too small for the
|
||||
provided image.
|
||||
:raises: ImageWriteError if writing the image to disk encounters any error.
|
||||
"""
|
||||
node_uuid = image_info.get('node_uuid')
|
||||
preserve_ep = image_info['preserve_ephemeral']
|
||||
configdrive = image_info['configdrive']
|
||||
|
@ -78,6 +102,17 @@ def _write_partition_image(image, image_info, device):
|
|||
|
||||
|
||||
def _write_whole_disk_image(image, image_info, device):
|
||||
"""Writes a whole disk image to the specified device.
|
||||
|
||||
:param image: Local path to image file to be written to the disk.
|
||||
:param image_info: Image information dictionary.
|
||||
This parameter is currently unused by the function.
|
||||
:param device: The device name, as a string, on which to store the image.
|
||||
Example: '/dev/sda'
|
||||
|
||||
:raises: ImageWriteError if the command to write the image encounters an
|
||||
error.
|
||||
"""
|
||||
script = _path_to_script('shell/write_image.sh')
|
||||
command = ['/bin/bash', script, image, device]
|
||||
LOG.info('Writing image with command: {0}'.format(' '.join(command)))
|
||||
|
@ -88,6 +123,14 @@ def _write_whole_disk_image(image, image_info, device):
|
|||
|
||||
|
||||
def _write_image(image_info, device):
|
||||
"""Writes an image to the specified device.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param device: The disk name, as a string, on which to store the image.
|
||||
Example: '/dev/sda'
|
||||
:raises: ImageWriteError if the command to write the image encounters an
|
||||
error.
|
||||
"""
|
||||
starttime = time.time()
|
||||
image = _image_location(image_info)
|
||||
uuids = {}
|
||||
|
@ -102,16 +145,34 @@ def _write_image(image_info, device):
|
|||
|
||||
|
||||
def _configdrive_is_url(configdrive):
|
||||
"""Determine if the configdrive location looks like an HTTP(S) URL.
|
||||
|
||||
:param configdrive: Location of the configdrive as a string.
|
||||
:returns: True if configdrive looks like an HTTP(S) URL, False otherwise.
|
||||
"""
|
||||
return (configdrive.startswith('http://')
|
||||
or configdrive.startswith('https://'))
|
||||
|
||||
|
||||
def _download_configdrive_to_file(configdrive, filename):
|
||||
"""Download the configdrive to a local file.
|
||||
|
||||
:param configdrive: The URL of the configdrive.
|
||||
:param filename: The filename of where to store the configdrive locally.
|
||||
"""
|
||||
content = requests.get(configdrive).content
|
||||
_write_configdrive_to_file(content, filename)
|
||||
|
||||
|
||||
def _write_configdrive_to_file(configdrive, filename):
|
||||
"""Writes the configdrive to a file.
|
||||
|
||||
Note that the contents of the configdrive are expected to be gzipped and
|
||||
base64 encoded.
|
||||
|
||||
:param configdrive: Contents of the configdrive file.
|
||||
:param filename: The filename of where to write the configdrive.
|
||||
"""
|
||||
LOG.debug('Writing configdrive to {0}'.format(filename))
|
||||
# configdrive data is base64'd, decode it first
|
||||
data = six.StringIO(base64.b64decode(configdrive))
|
||||
|
@ -122,6 +183,17 @@ def _write_configdrive_to_file(configdrive, filename):
|
|||
|
||||
|
||||
def _write_configdrive_to_partition(configdrive, device):
|
||||
"""Writes the configdrive to a partition on the given device.
|
||||
|
||||
:param configdrive: A string containing the location of the config drive
|
||||
as a URL OR the contents of the configdrive which
|
||||
must be gzipped and base64 encoded.
|
||||
:param device: The disk name, as a string, on which to store the image.
|
||||
Example: '/dev/sda'
|
||||
|
||||
:raises: ConfigDriveTooLargeError if the configdrive contents are too
|
||||
large to store on the given device.
|
||||
"""
|
||||
filename = _configdrive_location()
|
||||
if _configdrive_is_url(configdrive):
|
||||
_download_configdrive_to_file(configdrive, filename)
|
||||
|
@ -185,6 +257,23 @@ class ImageDownload(object):
|
|||
"""
|
||||
|
||||
def __init__(self, image_info, time_obj=None):
|
||||
"""Initialize an instance of the ImageDownload class.
|
||||
|
||||
Trys each URL in image_info successively until a URL returns a
|
||||
successful request code. Once the object is initialized, the user may
|
||||
retrieve chunks of the image through the standard python iterator
|
||||
interface until either the image is fully downloaded, or an error is
|
||||
encountered.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param time_obj: Optional time object to indicate when the image
|
||||
download began. Defaults to None. If None, then
|
||||
time.time() will be used to find the start time of
|
||||
the download.
|
||||
|
||||
:raises: ImageDownloadError if starting the image download fails for
|
||||
any reason.
|
||||
"""
|
||||
self._md5checksum = hashlib.md5()
|
||||
self._time = time_obj or time.time()
|
||||
self._request = None
|
||||
|
@ -206,6 +295,14 @@ class ImageDownload(object):
|
|||
raise errors.ImageDownloadError(image_info['id'], msg)
|
||||
|
||||
def _download_file(self, image_info, url):
|
||||
"""Opens a download stream for the given URL.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param url: The URL string to request the image from.
|
||||
|
||||
:raises: ImageDownloadError if the download stream was not started
|
||||
properly.
|
||||
"""
|
||||
no_proxy = image_info.get('no_proxy')
|
||||
if no_proxy:
|
||||
os.environ['no_proxy'] = no_proxy
|
||||
|
@ -218,15 +315,39 @@ class ImageDownload(object):
|
|||
return resp
|
||||
|
||||
def __iter__(self):
|
||||
"""Downloads and returns the next chunk of the image.
|
||||
|
||||
:returns: A chunk of the image. Size of chunk is IMAGE_CHUNK_SIZE
|
||||
which is a constant in this module.
|
||||
"""
|
||||
for chunk in self._request.iter_content(IMAGE_CHUNK_SIZE):
|
||||
self._md5checksum.update(chunk)
|
||||
yield chunk
|
||||
|
||||
def md5sum(self):
|
||||
"""Computes and returns the md5 checksum of the downloaded image.
|
||||
|
||||
Note that md5sum will not return the true checksum of the image until
|
||||
the download has been fully completed through this object's
|
||||
iterator inferface.
|
||||
|
||||
:returns: The md5 checksum of the image as a string in hexadecimal.
|
||||
"""
|
||||
return self._md5checksum.hexdigest()
|
||||
|
||||
|
||||
def _verify_image(image_info, image_location, checksum):
|
||||
"""Verifies the checksum of the local images matches expectations.
|
||||
|
||||
If this function does not raise ImageChecksumError then it is very likely
|
||||
that the local copy of the image was transmitted and stored correctly.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param image_location: The location of the local image.
|
||||
:param checksum: The computed checksum of the local image.
|
||||
:raises: ImageChecksumError if the checksum of the local image does not
|
||||
match the checksum as reported by glance in image_info.
|
||||
"""
|
||||
LOG.debug('Verifying image at {0} against MD5 checksum '
|
||||
'{1}'.format(image_location, checksum))
|
||||
if checksum != image_info['checksum']:
|
||||
|
@ -238,6 +359,13 @@ def _verify_image(image_info, image_location, checksum):
|
|||
|
||||
|
||||
def _download_image(image_info):
|
||||
"""Downloads the specified image to the local file system.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:raises: ImageDownloadError if the image download fails for any reason.
|
||||
:raises: ImageChecksumError if the downloaded image's checksum does not
|
||||
match the one reported in image_info.
|
||||
"""
|
||||
starttime = time.time()
|
||||
image_location = _image_location(image_info)
|
||||
image_download = ImageDownload(image_info, time_obj=starttime)
|
||||
|
@ -258,6 +386,17 @@ def _download_image(image_info):
|
|||
|
||||
|
||||
def _validate_image_info(ext, image_info=None, **kwargs):
|
||||
"""Validates the image_info dictionary has all required information.
|
||||
|
||||
:param ext: Object 'self'. Unused by this function directly, but left for
|
||||
compatibility with async_command validation.
|
||||
:param image_info: Image information dictionary.
|
||||
:param kwargs: Additional keyword arguments. Unused, but here for
|
||||
compatibility with async_command validation.
|
||||
:raises: InvalidCommandParamsError if the data contained in image_info
|
||||
does not match type and key:value pair requirements and
|
||||
expectations.
|
||||
"""
|
||||
image_info = image_info or {}
|
||||
|
||||
for field in ['id', 'urls', 'checksum']:
|
||||
|
@ -276,18 +415,44 @@ def _validate_image_info(ext, image_info=None, **kwargs):
|
|||
|
||||
|
||||
class StandbyExtension(base.BaseAgentExtension):
|
||||
"""Extension which adds stand-by related functionality to agent."""
|
||||
def __init__(self, agent=None):
|
||||
"""Constructs an instance of StandbyExtension.
|
||||
|
||||
:param agent: An optional IronicPythonAgent object. Defaults to None.
|
||||
"""
|
||||
super(StandbyExtension, self).__init__(agent=agent)
|
||||
|
||||
self.cached_image_id = None
|
||||
self.partition_uuids = None
|
||||
|
||||
def _cache_and_write_image(self, image_info, device):
|
||||
"""Cache an image and write it to a local device.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param device: The disk name, as a string, on which to store the
|
||||
image. Example: '/dev/sda'
|
||||
|
||||
:raises: ImageDownloadError if the image download fails for any reason.
|
||||
:raises: ImageChecksumError if the downloaded image's checksum does not
|
||||
match the one reported in image_info.
|
||||
:raises: ImageWriteError if writing the image fails.
|
||||
"""
|
||||
_download_image(image_info)
|
||||
self.partition_uuids = _write_image(image_info, device)
|
||||
self.cached_image_id = image_info['id']
|
||||
|
||||
def _stream_raw_image_onto_device(self, image_info, device):
|
||||
"""Streams raw image data to specified local device.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param device: The disk name, as a string, on which to store the
|
||||
image. Example: '/dev/sda'
|
||||
|
||||
:raises: ImageDownloadError if the image download encounters an error.
|
||||
:raises: ImageChecksumError if the checksum of the local image does not
|
||||
match the checksum as reported by glance in image_info.
|
||||
"""
|
||||
starttime = time.time()
|
||||
image_download = ImageDownload(image_info, time_obj=starttime)
|
||||
|
||||
|
@ -308,6 +473,18 @@ class StandbyExtension(base.BaseAgentExtension):
|
|||
|
||||
@base.async_command('cache_image', _validate_image_info)
|
||||
def cache_image(self, image_info=None, force=False):
|
||||
"""Asynchronously caches specified image to the local OS device.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param force: Optional. If True forces cache_image to download and
|
||||
cache image, even if the same image already exists on
|
||||
the local OS install device. Defaults to False.
|
||||
|
||||
:raises: ImageDownloadError if the image download fails for any reason.
|
||||
:raises: ImageChecksumError if the downloaded image's checksum does not
|
||||
match the one reported in image_info.
|
||||
:raises: ImageWriteError if writing the image fails.
|
||||
"""
|
||||
LOG.debug('Caching image %s', image_info['id'])
|
||||
device = hardware.dispatch_to_managers('get_os_install_device')
|
||||
|
||||
|
@ -329,6 +506,26 @@ class StandbyExtension(base.BaseAgentExtension):
|
|||
def prepare_image(self,
|
||||
image_info=None,
|
||||
configdrive=None):
|
||||
"""Asynchronously prepares specified image on local OS install device.
|
||||
|
||||
In this case, 'prepare' means make local machine completely ready to
|
||||
reboot to the image specified by image_info.
|
||||
|
||||
Downloads and writes an image to disk if necessary. Also writes a
|
||||
configdrive to disk if the configdrive parameter is specified.
|
||||
|
||||
:param image_info: Image information dictionary.
|
||||
:param configdrive: A string containing the location of the config
|
||||
drive as a URL OR the contents (as gzip/base64)
|
||||
of the configdrive. Optional, defaults to None.
|
||||
|
||||
:raises: ImageDownloadError if the image download encounters an error.
|
||||
:raises: ImageChecksumError if the checksum of the local image does not
|
||||
match the checksum as reported by glance in image_info.
|
||||
:raises: ImageWriteError if writing the image fails.
|
||||
:raises: ConfigDriveTooLargeError if the configdrive contents are too
|
||||
large to store on the given device.
|
||||
"""
|
||||
LOG.debug('Preparing image %s', image_info['id'])
|
||||
device = hardware.dispatch_to_managers('get_os_install_device')
|
||||
|
||||
|
@ -360,6 +557,19 @@ class StandbyExtension(base.BaseAgentExtension):
|
|||
return result_msg
|
||||
|
||||
def _run_shutdown_script(self, parameter):
|
||||
"""Runs the agent's shutdown script with the specified parameter.
|
||||
|
||||
The script only takes the following parameters:
|
||||
|
||||
-h : 'halts' the machine by turning the power off.
|
||||
-r : 'runs' the installed image by rebooting the machine.
|
||||
|
||||
Only one parameter should be specified.
|
||||
|
||||
:param parameter: The parameter to send to the shutdown script.
|
||||
:raises: SystemRebootError if calling the shutdown script returns an
|
||||
unsuccessful exit code.
|
||||
"""
|
||||
script = _path_to_script('shell/shutdown.sh')
|
||||
command = ['/bin/bash', script, parameter]
|
||||
# this should never return if successful
|
||||
|
@ -370,11 +580,13 @@ class StandbyExtension(base.BaseAgentExtension):
|
|||
|
||||
@base.async_command('run_image')
|
||||
def run_image(self):
|
||||
"""Runs image on agent's system via reboot."""
|
||||
LOG.info('Rebooting system')
|
||||
self._run_shutdown_script('-r')
|
||||
|
||||
@base.async_command('power_off')
|
||||
def power_off(self):
|
||||
"""Powers off the agent's system."""
|
||||
LOG.info('Powering off system')
|
||||
self._run_shutdown_script('-h')
|
||||
|
||||
|
|
Loading…
Reference in New Issue