# Copyright (c) 2014-2016 Hewlett Packard Enterprise Development LP
#
# Redistribution and use of this software in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import yaml
from euca2ools.commands.euimage.pack.profiles import build_image_profile
from euca2ools.util import check_dict_whitelist
class ImagePackMetadata(object):
def __init__(self):
self.image_sha256sum = None
self.image_size = None
self.image_md_sha256sum = None
self.version = 1 # Bump this with each incompatible change
@classmethod
def from_fileobj(cls, fileobj):
new_md = cls()
metadata = yaml.safe_load(fileobj)
check_dict_whitelist(metadata, 'pack', ['image', 'image_metadata',
'version'])
if metadata.get('version'):
# This is the version of the pack metadata, not the image.
# If we make backwards-incompatible changes this allows us
# to tell what to expect so we can continue to handle packs
# that precede those changes.
#
# Because there is only one metadata version right now this
# method is rather dumb -- it accepts only version 1.
if int(metadata['version']) != 1:
raise ValueError('pack has metadata version {0}; expected 1'
.format(metadata['version']))
image_info = metadata.get('image') or {}
if not image_info.get('sha256sum'):
raise ValueError('pack: image.sha256sum is missing or empty')
new_md.image_sha256sum = image_info['sha256sum']
if not image_info.get('size'):
raise ValueError('pack: image.size is missing or zero')
new_md.image_size = int(image_info['size'])
image_md_info = metadata.get('image_metadata') or {}
if not image_md_info.get('sha256sum'):
raise ValueError(
'pack: image_metadata.sha256sum is missing or empty')
new_md.image_md_sha256sum = image_md_info['sha256sum']
return new_md
@classmethod
def from_file(cls, filename):
with open(filename) as fileobj:
return cls.from_fileobj(fileobj)
def dump_to_fileobj(self, fileobj):
yaml.safe_dump(self.__serialize_as_dict(), fileobj,
default_flow_style=False)
def dump_to_file(self, filename):
with open(filename, 'w') as fileobj:
self.dump_to_fileobj(fileobj)
def __serialize_as_dict(self):
return {'image': {'sha256sum': self.image_sha256sum,
'size': self.image_size},
'image_metadata': {'sha256sum': self.image_md_sha256sum},
'version': self.version}
class ImageMetadata(object):
def __init__(self):
self.name = None
self.version = None
self.release = None
self.epoch = 0
self.arch = None
self.description = None
self.profiles = {}
@classmethod
def from_fileobj(cls, fileobj):
new_md = cls()
metadata = yaml.safe_load(fileobj)
check_dict_whitelist(metadata, 'image',
['name', 'version', 'release', 'arch',
'description', 'profiles'])
if not metadata.get('name'):
raise ValueError('name is missing or empty')
new_md.name = metadata['name']
if not metadata.get('version'):
raise ValueError('image "{0}": version is missing or empty'
.format(new_md.name))
new_md.version = metadata['version']
if not metadata.get('release'):
raise ValueError('image "{0}": release is missing or empty'
.format(new_md.name))
new_md.release = metadata['release']
if metadata.get('epoch'):
try:
new_md.epoch = int(metadata['epoch'])
except ValueError:
raise ValueError('image "{0}": epoch must be an integer'
.format(new_md.name))
if new_md.epoch < 0:
raise ValueError('image "{0}": epoch must not be negative'
.format(new_md.name))
if not metadata.get('arch'):
raise ValueError('image "{0}": arch is missing or empty'
.format(new_md.name))
new_md.arch = metadata['arch']
if not metadata.get('description'):
raise ValueError('image "{0}": description is missing or empty'
.format(new_md.name))
new_md.description = metadata['description'].rstrip()
profiles = metadata.get('profiles')
if not profiles:
raise ValueError('image "{0}" must have at least one profile '
'(use "default" for a single-profile image)'
.format(new_md.name))
if not isinstance(profiles, dict):
raise ValueError('image "{0}": profiles must be an associative '
'array'.format(new_md.name))
for profile_name, profile_info in profiles.items():
new_md.profiles[profile_name] = build_image_profile(profile_info,
new_md.arch)
return new_md
@classmethod
def from_file(cls, filename):
with open(filename) as fileobj:
return cls.from_fileobj(fileobj)
def install_profile(self, profile_name, services, image_fileobj,
image_size, args):
# Since different profiles can require different args to install
# correctly, we can't easily pick out the correct ones ahead
# of time. For simplicity's sake, we just pass everything and
# let the profile grab what it needs. Validation is the job of
# the profile, which will probably simply delegate that work to
# the commands it runs.
euimage_tags = {'euimage:name': self.name,
'euimage:version': self.version,
'euimage:release': self.release,
'euimage:profile': profile_name}
if self.epoch:
euimage_tags['euimage:epoch'] = self.epoch
if profile_name not in self.profiles:
raise ValueError('no such profile: "{0}"'.format(profile_name))
return self.profiles[profile_name].install(
self, services, image_fileobj, image_size, args, tags=euimage_tags)
def get_nvra(self):
return '{0}-{1}-{2}.{3}'.format(self.name, self.version, self.release,
self.arch)