class Recipes(RPCRoot): # For XMLRPC methods in this class. exposed = True hidden_id = widgets.HiddenField(name='id') confirm = widgets.Label( name='confirm', default="Are you sure you want to release the system?") return_reservation_form = widgets.TableForm( 'end_recipe_reservation', fields=[hidden_id, confirm], action='./really_return_reservation', submit_text=_(u'Yes')) tasks = RecipeTasks() recipe_widget = RecipeWidget() log_types = dict( R=LogRecipe, T=LogRecipeTask, E=LogRecipeTaskResult, ) @cherrypy.expose @identity.require(identity.not_anonymous()) def by_log_server(self, server, limit=50): """ Returns a list of recipe IDs which have logs stored on the given server. By default, returns at most 50 at a time. Only returns recipes where the whole recipe set has completed. Also excludes recently completed recipe sets, since the system may continue uploading logs for a short while until beaker-provision powers it off. """ finish_threshold = datetime.utcnow() - timedelta(minutes=2) recipes = Recipe.query.join(Recipe.recipeset)\ .join(RecipeSet.job)\ .filter(not_(Job.is_deleted))\ .filter(RecipeSet.status.in_([s for s in TaskStatus if s.finished]))\ .filter(not_(RecipeSet.recipes.any(Recipe.finish_time >= finish_threshold)))\ .filter(Recipe.log_server == server)\ .limit(limit) return [recipe_id for recipe_id, in recipes.values(Recipe.id)] @cherrypy.expose @identity.require(identity.not_anonymous()) def register_file(self, server, recipe_id, path, filename, basepath): """ register file and return path to store """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) if recipe.is_finished(): raise BX('Cannot register file for finished recipe %s' % recipe.t_id) # Add the log to the DB if it hasn't been recorded yet. log_recipe = LogRecipe.lazy_create( recipe_id=recipe.id, path=path, filename=filename, ) log_recipe.server = server log_recipe.basepath = basepath # Pull log_server out of server_url. recipe.log_server = urlparse.urlparse(server)[1] return '%s' % recipe.filepath @cherrypy.expose @identity.require(identity.not_anonymous()) def files(self, recipe_id): """ Return an array of logs for the given recipe. :param recipe_id: id of recipe :type recipe_id: integer .. deprecated:: 0.9.4 Use :meth:`taskactions.files() <bkr.server.task_actions.taskactions.files>` instead. """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) # Build a list of logs excluding duplicate paths, to mitigate: # https://bugzilla.redhat.com/show_bug.cgi?id=963492 logdicts = [] seen_paths = set() for log in recipe.all_logs(): logdict = log.dict # The path we care about here is the path which beaker-transfer # will move the file to. # Don't be tempted to use os.path.join() here since log['path'] # is often '/' which does not give the result you would expect. path = os.path.normpath( '%s/%s/%s' % (logdict['filepath'], logdict['path'], logdict['filename'])) if path in seen_paths: logger.warn('%s contains duplicate log %s', log.parent.t_id, path) else: seen_paths.add(path) logdicts.append(logdict) return logdicts @cherrypy.expose @identity.require(identity.in_group('lab_controller')) def change_files(self, recipe_id, server, basepath): """ Change the server and basepath where the log files lives, Usually used to move from lab controller cache to archive storage. """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) for mylog in recipe.all_logs(): mylog.server = '%s/%s/' % (server, mylog.parent.filepath) mylog.basepath = '%s/%s/' % (basepath, mylog.parent.filepath) recipe.log_server = urlparse.urlparse(server)[1] return True @cherrypy.expose @identity.require(identity.not_anonymous()) def extend(self, recipe_id, kill_time): """ Extend recipe watchdog by kill_time seconds """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) return recipe.extend(kill_time) @cherrypy.expose def console_output(self, recipe_id, output_length=None, offset=None): """ Get text console log output from OpenStack """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) manager = dynamic_virt.VirtManager(recipe.recipeset.job.owner) return manager.get_console_output(recipe.resource.instance_id, output_length) @cherrypy.expose def watchdog(self, recipe_id): try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) return recipe.status_watchdog() @cherrypy.expose @identity.require(identity.not_anonymous()) def stop(self, recipe_id, stop_type, msg=None): """ Set recipe status to Completed """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) if stop_type not in recipe.stop_types: raise BX( _('Invalid stop_type: %s, must be one of %s' % (stop_type, recipe.stop_types))) kwargs = dict(msg=msg) return getattr(recipe, stop_type)(**kwargs) @cherrypy.expose @identity.require(identity.not_anonymous()) def install_start(self, recipe_id=None): """ Records the start of a recipe's installation. The watchdog is extended by 3 hours to allow the installation to complete. """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % recipe_id)) if not recipe.installation: raise BX(_('Recipe %s not provisioned yet') % recipe_id) installation = recipe.installation if not installation.install_started: installation.install_started = datetime.utcnow() # extend watchdog by 3 hours 60 * 60 * 3 kill_time = 10800 logger.debug('Extending watchdog for %s', recipe.t_id) recipe.extend(kill_time) return True else: logger.debug('Already recorded install_started for %s', recipe.t_id) return False @cherrypy.expose @identity.require(identity.not_anonymous()) def postinstall_done(self, recipe_id=None): """ Report completion of postinstallation """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_(u'Invalid Recipe ID %s' % recipe_id)) if not recipe.installation: raise BX(_('Recipe %s not provisioned yet') % recipe_id) recipe.installation.postinstall_finished = datetime.utcnow() return True @cherrypy.expose @identity.require(identity.not_anonymous()) def install_done(self, recipe_id=None, fqdn=None): """ Report completion of installation with current FQDN """ if not recipe_id: raise BX(_("No recipe id provided!")) try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % recipe_id)) if not recipe.installation: raise BX(_('Recipe %s not provisioned yet') % recipe_id) recipe.installation.install_finished = datetime.utcnow() # We don't want to change an existing FQDN, just set it # if it hasn't been set already (see BZ#879146) configured = recipe.resource.fqdn if configured is None and fqdn: recipe.resource.fqdn = configured = fqdn elif configured != fqdn: # We use eager formatting here to make this easier to test logger.info("Configured FQDN (%s) != reported FQDN (%s) in R:%s" % (configured, fqdn, recipe_id)) return configured @identity.require(identity.not_anonymous()) @expose() def really_return_reservation(self, id, msg=None): try: recipe = Recipe.by_id(id) except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % id)) recipe.return_reservation() flash(_(u"Successfully released reserved system for %s" % recipe.t_id)) redirect('/jobs/mine') @expose(template="bkr.server.templates.form") @identity.require(identity.not_anonymous()) def return_reservation(self, recipe_id=None): """ End recipe reservation """ if not recipe_id: raise BX(_("No recipe id provided!")) return dict( title='Release reserved system for Recipe %s' % recipe_id, form=self.return_reservation_form, action='./really_return_reservation', options={}, value=dict(id=recipe_id), ) @cherrypy.expose @identity.require(identity.not_anonymous()) def postreboot(self, recipe_id=None): # Backwards compat only, delete this after 0.10: # the recipe_id arg used to be hostname try: int(recipe_id) except ValueError: system = System.by_fqdn(recipe_id, identity.current.user) system.action_power('reboot', service=u'XMLRPC', delay=30) return system.fqdn try: recipe = Recipe.by_id(int(recipe_id)) except (InvalidRequestError, NoResultFound, ValueError): raise BX(_('Invalid recipe ID %s') % recipe_id) if isinstance(recipe.resource, SystemResource): recipe.resource.system.action_power('reboot', service=u'XMLRPC', delay=30) return True @cherrypy.expose def to_xml(self, recipe_id=None): """ Pass in recipe id and you'll get that recipe's xml """ if not recipe_id: raise BX(_("No recipe id provided!")) try: recipexml = etree.tostring(Recipe.by_id(recipe_id).to_xml(), pretty_print=True, encoding='utf8') except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % recipe_id)) return recipexml def _recipe_search(self, recipe, **kw): recipe_search = search_utility.Recipe.search(recipe) for search in kw['recipesearch']: col = search['table'] try: recipe_search.append_results(search['value'], col, search['operation'], **kw) except KeyError, e: logger.error(e) return recipe_search.return_results() return recipe_search.return_results()
class Recipes(RPCRoot): # For XMLRPC methods in this class. exposed = True hidden_id = widgets.HiddenField(name='id') confirm = widgets.Label( name='confirm', default="Are you sure you want to release the system?") return_reservation_form = widgets.TableForm( 'end_recipe_reservation', fields=[hidden_id, confirm], action='./really_return_reservation', submit_text=_(u'Yes')) tasks = RecipeTasks() recipe_widget = RecipeWidget() log_types = dict( R=LogRecipe, T=LogRecipeTask, E=LogRecipeTaskResult, ) @cherrypy.expose @identity.require(identity.not_anonymous()) def by_log_server(self, server, limit=50): """ Returns a list of recipe IDs which have logs stored on the given server. By default, returns at most 50 at a time. Only returns recipes where the whole recipe set has completed. Also excludes recently completed recipe sets, since the system may continue uploading logs for a short while until beaker-provision powers it off. """ finish_threshold = datetime.utcnow() - timedelta(minutes=2) recipes = Recipe.query.join(Recipe.recipeset)\ .filter(RecipeSet.status.in_([s for s in TaskStatus if s.finished]))\ .filter(not_(RecipeSet.recipes.any(Recipe.finish_time >= finish_threshold)))\ .filter(Recipe.log_server == server)\ .limit(limit) return [recipe_id for recipe_id, in recipes.values(Recipe.id)] @cherrypy.expose @identity.require(identity.not_anonymous()) def register_file(self, server, recipe_id, path, filename, basepath): """ register file and return path to store """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) if recipe.is_finished(): raise BX('Cannot register file for finished recipe %s' % recipe.t_id) # Add the log to the DB if it hasn't been recorded yet. log_recipe = LogRecipe.lazy_create( recipe_id=recipe.id, path=path, filename=filename, ) log_recipe.server = server log_recipe.basepath = basepath # Pull log_server out of server_url. recipe.log_server = urlparse.urlparse(server)[1] return '%s' % recipe.filepath @cherrypy.expose @identity.require(identity.not_anonymous()) def files(self, recipe_id): """ Return an array of logs for the given recipe. :param recipe_id: id of recipe :type recipe_id: integer .. deprecated:: 0.9.4 Use :meth:`taskactions.files` instead. """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) return [log for log in recipe.all_logs] @cherrypy.expose @identity.require(identity.not_anonymous()) def change_files(self, recipe_id, server, basepath): """ Change the server and basepath where the log files lives, Usually used to move from lab controller cache to archive storage. """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) for mylog in recipe.all_logs: myserver = '%s/%s/' % (server, mylog['filepath']) mybasepath = '%s/%s/' % (basepath, mylog['filepath']) self.change_file(mylog['tid'], myserver, mybasepath) recipe.log_server = urlparse.urlparse(server)[1] return True @cherrypy.expose @identity.require(identity.not_anonymous()) def change_file(self, tid, server, basepath): """ Change the server and basepath where the log file lives, Usually used to move from lab controller cache to archive storage. """ log_type, log_id = tid.split(":") if log_type.upper() in self.log_types.keys(): try: mylog = self.log_types[log_type.upper()].by_id(log_id) except InvalidRequestError: raise BX(_("Invalid %s" % tid)) mylog.server = server mylog.basepath = basepath return True @cherrypy.expose @identity.require(identity.not_anonymous()) def extend(self, recipe_id, kill_time): """ Extend recipe watchdog by kill_time seconds """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) return recipe.extend(kill_time) @cherrypy.expose def console_output(self, recipe_id, output_length=None, offset=None): """ Get text console log output from OpenStack """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) manager = dynamic_virt.VirtManager(recipe.recipeset.job.owner) return manager.get_console_output(recipe.resource.instance_id, output_length) @cherrypy.expose def watchdog(self, recipe_id): try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) return recipe.status_watchdog() @cherrypy.expose @identity.require(identity.not_anonymous()) def stop(self, recipe_id, stop_type, msg=None): """ Set recipe status to Completed """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_('Invalid recipe ID: %s' % recipe_id)) if stop_type not in recipe.stop_types: raise BX( _('Invalid stop_type: %s, must be one of %s' % (stop_type, recipe.stop_types))) kwargs = dict(msg=msg) return getattr(recipe, stop_type)(**kwargs) @cherrypy.expose @identity.require(identity.not_anonymous()) def install_start(self, recipe_id=None): """ Report comencement of provisioning of a recipe's resource, extend first task's watchdog, and report 'Install Started' against it. """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % recipe_id)) first_task = recipe.first_task if not recipe.resource.install_started: recipe.resource.install_started = datetime.utcnow() # extend watchdog by 3 hours 60 * 60 * 3 kill_time = 10800 # XXX In future releases where 'Provisioning' # is a valid recipe state, we will no longer # need the following block. log.debug('Extending watchdog for %s', first_task.t_id) first_task.extend(kill_time) log.debug('Recording /start for %s', first_task.t_id) first_task.pass_(path=u'/start', score=0, summary=u'Install Started') return True else: log.debug('Already recorded /start for %s', first_task.t_id) return False @cherrypy.expose @identity.require(identity.not_anonymous()) def postinstall_done(self, recipe_id=None): """ Report completion of postinstallation """ try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_(u'Invalid Recipe ID %s' % recipe_id)) recipe.resource.postinstall_finished = datetime.utcnow() return True @cherrypy.expose @identity.require(identity.not_anonymous()) def install_done(self, recipe_id=None, fqdn=None): """ Report completion of installation with current FQDN """ if not recipe_id: raise BX(_("No recipe id provided!")) if not fqdn: raise BX(_("No fqdn provided!")) try: recipe = Recipe.by_id(recipe_id) except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % recipe_id)) recipe.resource.install_finished = datetime.utcnow() # We don't want to change an existing FQDN, just set it # if it hasn't been set already (see BZ#879146) configured = recipe.resource.fqdn if configured is None: recipe.resource.fqdn = configured = fqdn elif configured != fqdn: # We use eager formatting here to make this easier to test log.info("Configured FQDN (%s) != reported FQDN (%s) in R:%s" % (configured, fqdn, recipe_id)) return configured @identity.require(identity.not_anonymous()) @expose() def really_return_reservation(self, id, msg=None): try: recipe = Recipe.by_id(id) except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % id)) recipe.return_reservation() flash(_(u"Successfully released reserved system for %s" % recipe.t_id)) redirect('/jobs/mine') @expose(template="bkr.server.templates.form") @identity.require(identity.not_anonymous()) def return_reservation(self, recipe_id=None): """ End recipe reservation """ if not recipe_id: raise BX(_("No recipe id provided!")) return dict( title='Release reserved system for Recipe %s' % recipe_id, form=self.return_reservation_form, action='./really_return_reservation', options={}, value=dict(id=recipe_id), ) @cherrypy.expose @identity.require(identity.not_anonymous()) def postreboot(self, recipe_id=None): # Backwards compat only, delete this after 0.10: # the recipe_id arg used to be hostname try: int(recipe_id) except ValueError: system = System.by_fqdn(recipe_id, identity.current.user) system.action_power('reboot', service=u'XMLRPC', delay=30) return system.fqdn try: recipe = Recipe.by_id(int(recipe_id)) except (InvalidRequestError, NoResultFound, ValueError): raise BX(_('Invalid recipe ID %s') % recipe_id) if isinstance(recipe.resource, SystemResource): recipe.resource.system.action_power('reboot', service=u'XMLRPC', delay=30) return True @cherrypy.expose def to_xml(self, recipe_id=None): """ Pass in recipe id and you'll get that recipe's xml """ if not recipe_id: raise BX(_("No recipe id provided!")) try: recipexml = Recipe.by_id(recipe_id).to_xml().toprettyxml() except InvalidRequestError: raise BX(_("Invalid Recipe ID %s" % recipe_id)) return recipexml def _recipe_search(self, recipe, **kw): recipe_search = search_utility.Recipe.search(recipe) for search in kw['recipesearch']: col = search['table'] try: recipe_search.append_results(search['value'], col, search['operation'], **kw) except KeyError, e: log.error(e) return recipe_search.return_results() return recipe_search.return_results()