Source code for lifted.upload

#
# Copyright (C) 2019 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

from datetime import datetime
import logging
from multiprocessing import current_process
import os
import signal
from uuid import uuid4

from ansible_runner.interface import run as ansible_run
from ansible_runner.exceptions import AnsibleRunnerException

log = logging.getLogger("lifted")


[docs]class Upload: """Represents an upload of an image to a cloud provider. Instances of this class are serialized as TOML and stored in the upload queue directory, which is /var/lib/lorax/upload/queue/ by default""" def __init__( self, uuid=None, provider_name=None, playbook_path=None, image_name=None, settings=None, creation_time=None, upload_log=None, upload_pid=None, image_path=None, status_callback=None, status=None, ): self.uuid = uuid or str(uuid4()) self.provider_name = provider_name self.playbook_path = playbook_path self.image_name = image_name self.settings = settings self.creation_time = creation_time or datetime.now().timestamp() self.upload_log = upload_log or "" self.upload_pid = upload_pid self.image_path = image_path if status: self.status = status else: self.set_status("WAITING", status_callback) def _log(self, message, callback=None): """Logs something to the upload log with an optional callback :param message: the object to log :type message: object :param callback: a function of the form callback(self) :type callback: function """ if message: messages = str(message).splitlines() # Log multi-line messages as individual log lines for m in messages: log.info(m) self.upload_log += f"{message}\n" if callback: callback(self)
[docs] def serializable(self): """Returns a representation of the object as a dict for serialization :returns: the object's __dict__ :rtype: dict """ return self.__dict__
[docs] def summary(self): """Return a dict with useful information about the upload :returns: upload information :rtype: dict """ return { "uuid": self.uuid, "status": self.status, "provider_name": self.provider_name, "image_name": self.image_name, "image_path": self.image_path, "creation_time": self.creation_time, "settings": self.settings, }
[docs] def set_status(self, status, status_callback=None): """Sets the status of the upload with an optional callback :param status: the new status :type status: str :param status_callback: a function of the form callback(self) :type status_callback: function """ self._log("Setting status to %s" % status) self.status = status if status_callback: status_callback(self)
[docs] def ready(self, image_path, status_callback): """Provide an image_path and mark the upload as ready to execute :param image_path: path of the image to upload :type image_path: str :param status_callback: a function of the form callback(self) :type status_callback: function """ self._log("Setting image_path to %s" % image_path) self.image_path = image_path if self.status == "WAITING": self.set_status("READY", status_callback)
[docs] def reset(self, status_callback): """Reset the upload so it can be attempted again :param status_callback: a function of the form callback(self) :type status_callback: function """ if self.is_cancellable(): raise RuntimeError(f"Can't reset, status is {self.status}!") if not self.image_path: raise RuntimeError("Can't reset, no image supplied yet!") # self.error = None self._log("Resetting state") self.set_status("READY", status_callback)
[docs] def is_cancellable(self): """Is the upload in a cancellable state? :returns: whether the upload is cancellable :rtype: bool """ return self.status in ("WAITING", "READY", "RUNNING")
[docs] def cancel(self, status_callback=None): """Cancel the upload. Sends a SIGINT to self.upload_pid. :param status_callback: a function of the form callback(self) :type status_callback: function """ if not self.is_cancellable(): raise RuntimeError(f"Can't cancel, status is already {self.status}!") if self.upload_pid: os.kill(self.upload_pid, signal.SIGINT) self.set_status("CANCELLED", status_callback)
[docs] def execute(self, status_callback=None): """Execute the upload. Meant to be called from a dedicated process so that the upload can be cancelled by sending a SIGINT to self.upload_pid. :param status_callback: a function of the form callback(self) :type status_callback: function """ if self.status != "READY": raise RuntimeError("This upload is not ready!") try: self.upload_pid = current_process().pid self.set_status("RUNNING", status_callback) self._log("Executing playbook.yml") # NOTE: event_handler doesn't seem to be called for playbook errors logger = lambda e: self._log(e["stdout"], status_callback) runner = ansible_run( playbook=self.playbook_path, extravars={ **self.settings, "image_name": self.image_name, "image_path": self.image_path, }, event_handler=logger, verbosity=2, ) # Try logging events and stats -- but they may not exist, so catch the error try: for e in runner.events: self._log("%s" % dir(e), status_callback) self._log("%s" % runner.stats, status_callback) except AnsibleRunnerException: self._log("%s" % runner.stdout.read(), status_callback) if runner.status == "successful": self.set_status("FINISHED", status_callback) else: self.set_status("FAILED", status_callback) except Exception: import traceback log.error(traceback.format_exc(limit=2))