# -*- coding: utf-8 -*- # Copyright (C) 2013 Yahoo! Inc. All Rights Reserved. # # 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 contextlib import six import stevedore.driver from taskflow import exceptions as exc from taskflow.openstack.common import importutils from taskflow.persistence import backends as p_backends from taskflow.utils import misc from taskflow.utils import persistence_utils as p_utils from taskflow.utils import reflection # NOTE(imelnikov): this is the entrypoint namespace, not the module namespace. ENGINES_NAMESPACE = 'taskflow.engines' def _fetch_factory(factory_name): try: return importutils.import_class(factory_name) except (ImportError, ValueError) as e: raise ImportError("Could not import factory %r: %s" % (factory_name, e)) def _fetch_validate_factory(flow_factory): if isinstance(flow_factory, six.string_types): factory_fun = _fetch_factory(flow_factory) factory_name = flow_factory else: factory_fun = flow_factory factory_name = reflection.get_callable_name(flow_factory) try: reimported = _fetch_factory(factory_name) assert reimported == factory_fun except (ImportError, AssertionError): raise ValueError('Flow factory %r is not reimportable by name %s' % (factory_fun, factory_name)) return (factory_name, factory_fun) def load(flow, store=None, flow_detail=None, book=None, engine_conf=None, backend=None, namespace=ENGINES_NAMESPACE, **kwargs): """Load a flow into an engine. This function creates and prepares engine to run the flow. All that is left is to run the engine with 'run()' method. Which engine to load is specified in 'engine_conf' parameter. It can be a string that names engine type or a dictionary which holds engine type (with 'engine' key) and additional engine-specific configuration. Which storage backend to use is defined by backend parameter. It can be backend itself, or a dictionary that is passed to taskflow.persistence.backends.fetch to obtain backend. :param flow: flow to load :param store: dict -- data to put to storage to satisfy flow requirements :param flow_detail: FlowDetail that holds the state of the flow (if one is not provided then one will be created for you in the provided backend) :param book: LogBook to create flow detail in if flow_detail is None :param engine_conf: engine type and configuration configuration :param backend: storage backend to use or configuration :param namespace: driver namespace for stevedore (default is fine if you don't know what is it) :returns: engine """ if engine_conf is None: engine_conf = {'engine': 'default'} # NOTE(imelnikov): this allows simpler syntax. if isinstance(engine_conf, six.string_types): engine_conf = {'engine': engine_conf} engine_name = engine_conf['engine'] try: pieces = misc.parse_uri(engine_name) except (TypeError, ValueError): pass else: engine_name = pieces['scheme'] engine_conf = misc.merge_uri(pieces, engine_conf.copy()) if isinstance(backend, dict): backend = p_backends.fetch(backend) if flow_detail is None: flow_detail = p_utils.create_flow_detail(flow, book=book, backend=backend) try: mgr = stevedore.driver.DriverManager( namespace, engine_name, invoke_on_load=True, invoke_args=(flow, flow_detail, backend, engine_conf), invoke_kwds=kwargs) engine = mgr.driver except RuntimeError as e: raise exc.NotFound("Could not find engine %s" % (engine_name), e) else: if store: engine.storage.inject(store) return engine def run(flow, store=None, flow_detail=None, book=None, engine_conf=None, backend=None, namespace=ENGINES_NAMESPACE, **kwargs): """Run the flow. This function load the flow into engine (with 'load' function) and runs the engine. Which engine to load is specified in 'engine_conf' parameter. It can be a string that names engine type or a dictionary which holds engine type (with 'engine' key) and additional engine-specific configuration. Which storage backend to use is defined by backend parameter. It can be backend itself, or a dictionary that is passed to taskflow.persistence.backends.fetch to obtain backend. :param flow: flow to run :param store: dict -- data to put to storage to satisfy flow requirements :param flow_detail: FlowDetail that holds the state of the flow (if one is not provided then one will be created for you in the provided backend) :param book: LogBook to create flow detail in if flow_detail is None :param engine_conf: engine type and configuration configuration :param backend: storage backend to use or configuration :param namespace: driver namespace for stevedore (default is fine if you don't know what is it) :returns: dictionary of all named task results (see Storage.fetch_all) """ engine = load(flow, store=store, flow_detail=flow_detail, book=book, engine_conf=engine_conf, backend=backend, namespace=namespace, **kwargs) engine.run() return engine.storage.fetch_all() def save_factory_details(flow_detail, flow_factory, factory_args, factory_kwargs, backend=None): """Saves the given factories reimportable attributes into the flow detail. This function saves the factory name, arguments, and keyword arguments into the given flow details object and if a backend is provided it will also ensure that the backend saves the flow details after being updated. :param flow_detail: FlowDetail that holds state of the flow to load :param flow_factory: function or string: function that creates the flow :param factory_args: list or tuple of factory positional arguments :param factory_kwargs: dict of factory keyword arguments :param backend: storage backend to use or configuration """ if not factory_args: factory_args = [] if not factory_kwargs: factory_kwargs = {} factory_name, _factory_fun = _fetch_validate_factory(flow_factory) factory_data = { 'factory': { 'name': factory_name, 'args': factory_args, 'kwargs': factory_kwargs, }, } if not flow_detail.meta: flow_detail.meta = factory_data else: flow_detail.meta.update(factory_data) if backend is not None: if isinstance(backend, dict): backend = p_backends.fetch(backend) with contextlib.closing(backend.get_connection()) as conn: conn.update_flow_details(flow_detail) def load_from_factory(flow_factory, factory_args=None, factory_kwargs=None, store=None, book=None, engine_conf=None, backend=None, namespace=ENGINES_NAMESPACE, **kwargs): """Loads a flow from a factory function into an engine. Gets flow factory function (or name of it) and creates flow with it. Then, flow is loaded into engine with load(), and factory function fully qualified name is saved to flow metadata so that it can be later resumed with resume. :param flow_factory: function or string: function that creates the flow :param factory_args: list or tuple of factory positional arguments :param factory_kwargs: dict of factory keyword arguments :param store: dict -- data to put to storage to satisfy flow requirements :param book: LogBook to create flow detail in :param engine_conf: engine type and configuration configuration :param backend: storage backend to use or configuration :param namespace: driver namespace for stevedore (default is fine if you don't know what is it) :returns: engine """ _factory_name, factory_fun = _fetch_validate_factory(flow_factory) if not factory_args: factory_args = [] if not factory_kwargs: factory_kwargs = {} flow = factory_fun(*factory_args, **factory_kwargs) if isinstance(backend, dict): backend = p_backends.fetch(backend) flow_detail = p_utils.create_flow_detail(flow, book=book, backend=backend) save_factory_details(flow_detail, flow_factory, factory_args, factory_kwargs, backend=backend) return load(flow=flow, store=store, flow_detail=flow_detail, book=book, engine_conf=engine_conf, backend=backend, namespace=namespace, **kwargs) def flow_from_detail(flow_detail): """Reloads a flow previously saved. Gets the flow factories name and any arguments and keyword arguments from the flow details metadata, and then calls that factory to recreate the flow. :param flow_detail: FlowDetail that holds state of the flow to load """ try: factory_data = flow_detail.meta['factory'] except (KeyError, AttributeError, TypeError): raise ValueError('Cannot reconstruct flow %s %s: ' 'no factory information saved.' % (flow_detail.name, flow_detail.uuid)) try: factory_fun = _fetch_factory(factory_data['name']) except (KeyError, ImportError): raise ImportError('Could not import factory for flow %s %s' % (flow_detail.name, flow_detail.uuid)) args = factory_data.get('args', ()) kwargs = factory_data.get('kwargs', {}) return factory_fun(*args, **kwargs) def load_from_detail(flow_detail, store=None, engine_conf=None, backend=None, namespace=ENGINES_NAMESPACE, **kwargs): """Reloads an engine previously saved. This reloads the flow using the flow_from_detail() function and then calls into the load() function to create an engine from that flow. :param flow_detail: FlowDetail that holds state of the flow to load :param store: dict -- data to put to storage to satisfy flow requirements :param engine_conf: engine type and configuration configuration :param backend: storage backend to use or configuration :param namespace: driver namespace for stevedore (default is fine if you don't know what is it) :returns: engine """ flow = flow_from_detail(flow_detail) return load(flow, flow_detail=flow_detail, store=store, engine_conf=engine_conf, backend=backend, namespace=namespace, **kwargs)