177 lines
7.5 KiB
Python
177 lines
7.5 KiB
Python
# coding=utf-8
|
|
|
|
# Copyright 2013 Red Hat, Inc.
|
|
#
|
|
# 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 logging
|
|
from OSInfo import OSInfo
|
|
from StackEnvironment import StackEnvironment
|
|
from time import sleep
|
|
|
|
|
|
class Builder(object):
|
|
def __init__(self, osid, install_location=None, install_type=None, install_script=None, install_config={}):
|
|
"""
|
|
Builder selects the correct OS object to delegate build activity to.
|
|
|
|
@param osid: The shortid for an OS record.
|
|
@param install_location: The location of an ISO or install tree.
|
|
@param install_type: The type of installation (iso or tree)
|
|
@param install_script: A custom install script to be used instead of what OSInfo can generate
|
|
@param install_config: A dict of various info that may be needed for the build.
|
|
(admin_pw, license_key, arch, disk_size, flavor, storage, name)
|
|
"""
|
|
super(Builder, self).__init__()
|
|
self.log = logging.getLogger('%s.%s' % (__name__, self.__class__.__name__))
|
|
self.install_location = install_location
|
|
self.install_type = install_type
|
|
self.install_script = install_script
|
|
self.install_config = install_config
|
|
self.os = OSInfo().os_for_shortid(osid)
|
|
self.os_delegate = self._delegate_for_os(self.os)
|
|
self.env = StackEnvironment()
|
|
|
|
def _delegate_for_os(self, os):
|
|
"""
|
|
Select and instantiate the correct OS class for build delegation.
|
|
|
|
@param os: The dictionary of OS info for a give OS shortid
|
|
|
|
@return: An instance of an OS class that will control a VM for the image installation
|
|
"""
|
|
# TODO: Change the way we select what class to instantiate to something that we do not have to touch
|
|
# every time we add another OS class
|
|
os_classes = {'fedora': 'RedHatOS', 'rhel': 'RedHatOS', 'win': 'WindowsOS', 'ubuntu': 'UbuntuOS'}
|
|
os_classname = os_classes.get(os['distro'])
|
|
|
|
if os_classname:
|
|
try:
|
|
os_module = __import__("novaimagebuilder." + os_classname, fromlist=[os_classname])
|
|
os_class = getattr(os_module, os_classname)
|
|
#import pdb; pdb.set_trace()
|
|
return os_class(osinfo_dict=self.os,
|
|
install_type=self.install_type,
|
|
install_media_location=self.install_location,
|
|
install_config=self.install_config,
|
|
install_script=self.install_script)
|
|
except ImportError as e:
|
|
self.log.exception(e)
|
|
return None
|
|
else:
|
|
raise Exception("No delegate found for distro (%s)" % os['distro'])
|
|
|
|
def run(self):
|
|
"""
|
|
Starts the installation of an OS in an image via the appropriate OS class
|
|
|
|
@return: Status of the installation.
|
|
"""
|
|
self.os_delegate.prepare_install_instance()
|
|
self.os_delegate.start_install_instance()
|
|
return self.os_delegate.update_status()
|
|
|
|
def wait_for_completion(self, inactivity_timeout):
|
|
"""
|
|
Waits for the install_instance to enter SHUTDOWN state then launches a snapshot
|
|
|
|
@param inactivity_timeout amount of time to wait for activity before declaring the installation a failure in 10s of seconds (6 is 60 seconds)
|
|
|
|
@return: Success or Failure
|
|
"""
|
|
# TODO: Timeouts, activity checking
|
|
instance = self._wait_for_shutoff(self.os_delegate.install_instance, inactivity_timeout)
|
|
# Snapshot with self.install_config['name']
|
|
if instance:
|
|
finished_image_id = instance.instance.create_image(self.install_config['name'])
|
|
self._wait_for_glance_snapshot(finished_image_id)
|
|
self._terminate_instance(instance.id)
|
|
if self.os_delegate.iso_volume_delete:
|
|
self.env.cinder.volumes.get(self.os_delegate.iso_volume).delete()
|
|
self.log.debug("Deleted install ISO volume from cinder: %s" % self.os_delegate.iso_volume)
|
|
# Leave instance running if install did not finish
|
|
|
|
def _wait_for_shutoff(self, instance, inactivity_timeout):
|
|
inactivity_countdown = inactivity_timeout
|
|
for i in range(1200):
|
|
status = instance.status
|
|
if status == "SHUTOFF":
|
|
self.log.debug("Instance (%s) has entered SHUTOFF state" % instance.id)
|
|
return instance
|
|
if i % 10 == 0:
|
|
self.log.debug("Waiting for instance status SHUTOFF - current status (%s): %d/1200" % (status, i))
|
|
if not instance.is_active():
|
|
inactivity_countdown -= 1
|
|
else:
|
|
inactivity_countdown = inactivity_timeout
|
|
if inactivity_countdown == 0:
|
|
self.log.debug("Install instance has become inactive. Instance will remain running so you can investigate what happened.")
|
|
return
|
|
sleep(1)
|
|
|
|
|
|
def _wait_for_glance_snapshot(self, image_id):
|
|
image = self.env.glance.images.get(image_id)
|
|
self.log.debug("Waiting for glance image id (%s) to become active" % image_id)
|
|
while True:
|
|
self.log.debug("Current image status: %s" % image.status)
|
|
sleep(2)
|
|
image = self.env.glance.images.get(image.id)
|
|
if image.status == "error":
|
|
raise Exception("Image entered error status while waiting for completion")
|
|
elif image.status == 'active':
|
|
break
|
|
# Remove any direct boot properties if they exist
|
|
properties = image.properties
|
|
for key in ['kernel_id', 'ramdisk_id', 'command_line']:
|
|
if key in properties:
|
|
del properties[key]
|
|
meta = {'properties': properties}
|
|
image.update(**meta)
|
|
|
|
def _terminate_instance(self, instance_id):
|
|
nova = self.env.nova
|
|
instance = nova.servers.get(instance_id)
|
|
instance.delete()
|
|
self.log.debug("Waiting for instance id (%s) to be terminated/delete" % instance_id)
|
|
while True:
|
|
self.log.debug("Current instance status: %s" % instance.status)
|
|
sleep(5)
|
|
try:
|
|
instance = nova.servers.get(instance_id)
|
|
except Exception as e:
|
|
self.log.debug("Got exception (%s) assuming deletion complete" % e)
|
|
break
|
|
|
|
def abort(self):
|
|
"""
|
|
Aborts the installation of an OS in an image.
|
|
|
|
@return: Status of the installation.
|
|
"""
|
|
self.os_delegate.abort()
|
|
self.os_delegate.cleanup()
|
|
return self.os_delegate.update_status()
|
|
|
|
def status(self):
|
|
"""
|
|
Returns the status of the installation.
|
|
|
|
@return: Status of the installation.
|
|
"""
|
|
# TODO: replace this with a background thread that watches the status and cleans up as needed.
|
|
status = self.os_delegate.update_status()
|
|
if status in ('COMPLETE', 'FAILED'):
|
|
self.os_delegate.cleanup()
|
|
return status
|