#!/usr/bin/env python
#Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
""" Blueprint Processor: Command line tool to instantiate Oracle Cloud blueprints."""

#
# Overview of bp_processor
#   . Globals
#   . Classes
#     . Exceptions
#     . Other classes: Simvalue, Timeline, CloudConnection, Blueprint 
#   . Non-class functions
#   . main
# Generally, we order functions and classes such that they appear before they 
# are called.
#
# The largest class is Blueprint.  It is organized into sections:
#    . Utility functions such as dump and is_in_sim_mode
#    . Cloud operations, which in turn use CloudConnection (unless simulated)
#    . Expression evaluation, e.g. find_one, fail_if, and get_ready_resource
#    . Path expression parsing/evaluation
#    . General function and expression evaluation, including fn_* functions 
#    . Interacting with user (aka "debugger")
#    . Initialization, e.g. init_*_section, init_oracle_macros, validate, and initialize*
#    . Process Inputs section, i.e. input parameters
#    . Process Resources section
#    . Process Outputs section
#    . Other high-level functions, e.g. 'instantiate_bp'
#
# Non-class functions are organized into sections too:
#    . Exception-related utility (raise_*, print_*, and show_*)
#    . Various other utility functions 
#    . Path-expression- or parsing-related
#    . Resource- or Blueprint-cognizant functions (and main)
#
# The primary runtime structure is the blueprint itself.  Knowledge of this 
# structure is shared by many routines in this module, not just those of the 
# Blueprint class.  
#
# The blueprint structure is augmented with runtime information:
#  . Input param _values_, in ['Inputs'][<paramName>]['Value']
#  . The runtime Cloud object, in ['Cloud']
#  . The environment information , in ['Info']
#  . Each resource spec:
#    . _uri: the URI returned as result of successful POST; None if no POST
#    . _cloud_status: the state returned by the cloud resource   
#    . _state: the 'resource state'
#    . _sub_state: ancillary state info, i.e. if pending, what it waits for
#    . _logged_state: last logged state (used by timeline code)
#    . _create_start_time: time when creation began
#    . _create_end_time: time when creation ended
#    . Simulation info: 
#      . delay: number of seconds to wait (can be provided in blueprint; defaults to 1)
#      . result: 's' or 'f' (can be provided in blueprint; defaults to 's')
#      . _finish_after: time after which 'delay' is over.  Computed when simulated
#                       creation is initiated
  
#---------------
# TODO: Enhancements to consider:
#       - handle non-scalar inputs, e.g. a list of values
#       - path expressions: assumes that member name syntax is that of <identifier>.  Should we
#                           support more general syntax, e.g. allow special chars to be escaped?
#---------------

#import bp_charter

import os
import shutil
import getpass
import sys
import socket
import copy
import json
import logging
import time
import base64
from optparse import OptionParser
import re
import ssl
import string
import uuid
import urllib
import optparse  # Only used for 1 constant to hide help for -v options

# The following are not from the Python standard library
import httplib2  # License: MIT
import yaml      # License: See http://pyyaml.org/browser/pyyaml/trunk/LICENSE

# Use of pydot is optional and requires customer installation.  It's only used for
# the visualization reports.  Note that importing pydot will result in printing "..."
# if dot_parser is not installed, but that's not a problem.
pydotx_not_imported = False
try:
    import pydot     # License: MIT
except ImportError:
    pydotx_not_imported = True

# todo: The following is intended to declare what is public from the module, but does it help? 
__all__ = ['BlueprintError', 'Blueprint']
  
#-----------------
# Module Constants

PROCESSOR_VERSION = '12.1.0.5, 30-Jan-2013'
X_SPEC_VERSION = '10001'

MAX_RETRIES = 3           # max number of times to retry when http response includes 'retry-after'
MAX_RETRY_PAUSE = 5       # max number of seconds to wait between retries
DEFAULT_REFRESH_FREQ = 15 # Emit a timeline row after this many seconds of inactivity

APP_DIRECTORY = sys.path[0]
LEGAL_SCALARS = [int, long, float, str, unicode, bool]                     

#-----------------
# Logging constants

# Use of logging levels is...
#   Info:  same as 'standard output'
#   Debug: greater detail for debugging
#   Warn:  info to user re possible issue
#   Error: info to user re error (in blueprint or cloud)
#   Critical: info to user re likely blueprint processor bug, i.e. internal issue

LOG_FORMAT = '%(asctime)s %(levelname)5s: %(message)s'  # Note: the '5' improves INFO/ERROR allignment
logging.basicConfig(format = LOG_FORMAT, datefmt='%H:%M:%S')
LOGGER = logging.getLogger('bp')
LOGGER.setLevel(logging.INFO)

# A "constant" only after the MIME abbrevs file is read:
_MIME_ABBREVS = None  # Will be a dict, populated from file.  See expand_mime_abbrev.

##########
#
# States of a resource (or resource definition)
#
# There are two kinds of state, 'cloud state' and 'resource state' (or simply 'state').  
#     - 'Cloud state' is the state returned from the cloud resource.
#       It's a value shown to the user for cloud-type-specific state information. 
#     - 'Resource state', often referred to as 'state' is a normalized state used
#       for blueprint processing.  Each cloud state is mapped to a resource state.
#       Not all resource states have a corresponding cloud state.  Some resource 
#       states refer to the state of a resource definition prior to requesting 
#       cloud resource creation, e.g. 'evaulating', 'eval pending', and 'eval failed'.
#       See the state constants below.
#
# Resource states:
#     EVAL_NOT_TRIED: initial state
#     EVAL_SUCCEEDED: evaluation of container and resource expressions succeeded
#     EVAL_FAILED: (not sure we need this state, except to diagnose offending res spec)
#     EVAL_PENDING:   evaluation of container and resource expressions pending
#     POST_FAILED:    post to create succeeded.
#     CREATING:       post to create succeeded.  Resource GET's may have been done in
#                     which case resource reports INITIATED, CREATING, or CREATED.
#                     Note: No need for a CREATED state; we only care about READY.
#     CREATE_FAILED:  resource reports EXCEPTION or UNKNOWN
#     READY:          resource reports READY
#
# Cloud resource states per spec:
#     INITIATED,
#     CREATING,
#     CREATED,
#     DESTROYING: only used after an http DELETE
#     DESTROYED:  only used after an http DELETE
#     OBSOLETE:   should never see
#     UNKNOWN,
#     EXCEPTION,
#     READY
#
# Notes:
#   'Evaluation' refers to evaluating a resource, including its Container and Properties sections.
#

# Resource states
_EVAL_NOT_TRIED = u'EVALUATING'   # -> evaluated | pending | evalFailed
_EVAL_SUCCEEDED = u'EVALUATED'    # -> creating (and _URI is set)
_EVAL_FAILED = u'EVAL_FAILED'     # -> (final)
_EVAL_PENDING = u'EVAL_PENDING'   # -> evaluated | pending | evalFailed
_POST_FAILED = u'POST_FAILED'     # -> (final)
_CREATING = u'CREATING'           # -> created | createFailed | INITIATED
_CREATE_FAILED = u'CREATE_FAILED' # -> (final)
_READY = u'READY'

# Cloud states, which are mapped/normalized to resource states (after _EVAL* states)
_CLOUD_INITIATED = u'INITIATED'
_CLOUD_CREATING = u'CREATING'
_CLOUD_CREATED = u'CREATED'
_CLOUD_UNKNOWN = u'UNKNOWN'
_CLOUD_EXCEPTION = u'EXCEPTION'
_CLOUD_READY = u'READY'

STATE_ABBREV_TABLE = {_EVAL_NOT_TRIED : 'e ',
                      _EVAL_SUCCEEDED : 'es',
                      _EVAL_FAILED : 'EF',
                      _EVAL_PENDING : 'ep',
                      _POST_FAILED : 'CF',  # todoWV: or should we distinguish between this and CF?
                      _CREATING : 'c ',
                      _CREATE_FAILED : 'CF',
                      _READY : 'CS'}
FINAL_STATES = [_EVAL_FAILED, _CREATE_FAILED, _READY]

# Visualization-related
LOGO_FILENAME = 'OracleLogo.png'
LEGEND_FILENAME = 'legend.png' 
IMAGE_FILENAME = 'image.png'
HTML_FILENAME = 'bp_report.html'
BULLET_STRING = '- '

# Blueprint exceptions
#
# Type hierarchy:
#  - Exception
#    - BlueprintException (abstract)
#      - BlueprintEvalPending
#      - BlueprintError (abstract)
#        - BlueprintEvalError
#        - BlueprintExecError
#
# All BlueprintExceptions have a 'msg'.  Some intrinsics support custom diagnostics that 
# are more specific than a generic diagnostic.  If so, both diagnostics are reflected in 
# the 'msg' of the exception.  (See raise_either_eval_diag.)
#
# BlueprintEvalPending is raised when evaluation of an expression cannot proceed.  Its msg explains why. 
#
# There are 2 kinds of BlueprintError:  
#  -  A BlueprintEvalError reflects an expression evaulation failure and has an 
#     expression stack, used to capture/dump the recursive evaluation of an expression.
#     Evaluation errors get reraised at each level of recursion.  In this way, we 
#     retain and add to the expression evaluation stack.  (See reraise_eval_error.)
#  -  BlueprintExecError is used for all other blueprint errors and needs no expression
#     evaluation stack.
#
# Coding conventions:
#   - Only raise BlueprintError exceptions via raise_* or reraise_* functions.  These 
#     functions take actions common to such exception raising, in particular logging.
#   - The top level functions called by main are 
#      - initialize: can throw BlueprintError
#      - instantiate_bp: catches exceptions and usually continues, for instance so
#        that Output section processing can be done even if Resource processing was
#        interrupted.  (Exceptions are appended to eaten_exceptions, so that test 
#        driver can verify.)

class BlueprintException(Exception):
    """Abstract blueprint exception.

    Attributes:
        msg  -- 
    """
    def __init__(self, msg):
        self.msg = msg
    def __repr__(self):
        return self.msg
    
    # Note: I don't thing __str__ shouldn be needed, because __str__ should
    #       revert to __repr__.  But function_based.py regression tests failed 
    #       until i restored __str__.
    def __str__(self):
        return self.msg

class BlueprintEvalPending(BlueprintException):
    """Exception raised when an expression evaluation depends on 'pending' info.

    Attributes:
        msg  -- indication of why we are waiting

    There are currently 3 reasons for waiting:
      - Resource not ready:
        . We haven't been able to attempt creation (and the res spec has no uri.
        . We have posted to create, but the resource is not READY
      - waitFor* intrinsic has not found a match
    """
    pass

class BlueprintError(BlueprintException):
    """Exception raised when expression evaluation fails or execution fails.

    Attributes:
        msg  -- explanation of the error
    """
    pass

class BlueprintEvalError(BlueprintError):
    """Exception raised when an expression evaluation fails, e.g. SSA issue
       such as request timed out, state returned from resource is EXCEPTION, ...

    Attributes:
        msg  -- explanation of the error
        expr_stack -- stack reflecting recursive evaluation of expression.  Will be 
                      read directly to display sequence of evaluations leading to error
    """
    def __init__(self, msg):
        super(BlueprintEvalError, self ).__init__(msg)
        self.expr_stack = []

    def __repr__(self):  #todo: revisit
        return 'Eval exception: {0}/{1}'.format(self.msg, self.expr_stack)
    
    def push_expr(self, expr):
        """ Push 'expr' onto the stack for later display.  'expr' must be a string."""
        self.expr_stack.append(str(expr))
        
class BlueprintExecError(BlueprintError):
    """Exception raised when execution fails for some non-expression evaluation 
       issue, such as invalid cloud credentials.

    Attributes:
        msg  -- explanation of the error
    """
    pass

################################################################################

class SimValue(yaml.YAMLObject):
    """The value of an expression may be a normal value or a simulation 'value',
       aka 'SimValue'.  A SimValue is used in simulation mode when the actual value
       cannot be determined.  For instance, consider a blueprint expression that uses 
       f_getResourceAttr.  In simulation mode, a blueprint may create a resource
       Foo and then request its 'bar' attribute value.  The SimValue will represent
       "attribute 'bar' of resource Foo".
       
       As expression evaluation proceeds, information may be appended
       to the SimValue.  For instance, a path expression like
          Cloud.zones.elements[0].uri->
       
       There is no Cloud to access, so the SimValue captures enough information
       so that the reader knows what the expression refers to. 
       
       Resource URIs are treated specially because operations on those
       have to be properly simulated.  When expression evaluation operates
       on a URI, callers get the resource name in order to access simulation
       settings such as delay and what result to simulate.  For instance, the
       use of f_getResourceAttr(myResource, attrName) maps to something like 
       this (simplified):
          Resources.myResource._uri->attrName
          
       The SimValue builds up a value like "Resources.myResource" and when the 
       "_uri" is encountered, it replaces this with "uri of myResource".
       """
    URI_OF_PREFIX = 'URI-of-'

    def __init__(self):
        self._value = []

    def __repr__(self):
        return 'SimValue: {' + ''.join(self._value) + '}'

    def __str__(self):
        return ''.join(self._value)

    def set_for_resource_uri(self, res_name):
        """ Set value to simulate a URI for a given resource. """
        self._value = [self.URI_OF_PREFIX, res_name]

    def get_for_resource_uri(self):
        """Return root resource for which the value is being derived, or ''. """
        if self._value[0] == self.URI_OF_PREFIX:
            return self._value[1]
        else:
            return ''

    def append(self, value):
        """Set value of sim value to 'value'.  (Assumed to be a string.)"""
        self._value.append(value)
        
#####

def sv_representer(dumper, data):
    """A YAML 'representer', used by yaml.dump() to represent SimValue's."""    
    return dumper.represent_scalar(u'SimulatedValue:', str(data))

yaml.add_representer(SimValue, sv_representer)

################################################################################

class Timeline():
    """Represents the timeline for creation of resources.  Shown in log."""    
    INDENT = '  '
    
    def __init__(self, refresh_frequency):
        # Note: 'resources' will be a pointer to the blueprint's Resource section
        self.refresh_frequency = refresh_frequency
        self.resources = {}
        self.most_recent_display_time = None

    def _print_col_headers(self):
        """ Emit column headers, oriented diagonally. """

        # Easier to emit rows bottom up:
        #  - Init list: 'headers'
        #  - Init string: 'slashes', centered on cols
        #  - While cols remain in 'headers', emit the lines into a list:
        #    - Print 'slashes'
        #    - Add space, replace last slash with Nth header
        #    - Print new version of 'slashes'
        #    - Remove header and trim right spaces
        #  - Print the lines of the list in reverse order
        # Then emit the horizontal line of the table

        headers = []
        i = 1
        for res_key in iter(sorted(self.resources)):
            self.resources[res_key]['_logger_resnum'] = i
            headers.append(res_key)
            i += 1

        LOGGER.info(' ')
        header_lines = []
        slashes = self.INDENT + ('  /  ' * len(self.resources)).rstrip()
        while len(headers) > 0:
            header_lines.append(slashes)
            last_header = headers.pop()
            slashes = ' ' + slashes[0 : len(slashes) - 1]
            header_lines.append(slashes + last_header)
            slashes = ' ' + slashes.rstrip()
        while len(header_lines) > 0:
            LOGGER.info(self.INDENT + header_lines.pop())        
        self._emit_table_line()

    def _emit_generic_line(self, res_name, text, row_suffix = ''):
        """ Emit a line in which the column for 'res_name' shows 'text'.
            (Value of 'res_name' may be '', in which case no resource has changed state. 
        Also:
            - Print '.' if creation is in progress for other columns.
            - Print 'row_suffix' at the end of the line.)
        """
        row = self.INDENT
        for res_key in iter(sorted(self.resources)):
            if res_key == res_name:
                row = row + '|' + text 
            else:
                if self.resources[res_key]['_state'] == _CREATING:
                    row = row + '| .  '
                else:
                    row = row + '|    '
        LOGGER.info(row + '| ' + row_suffix)        
        self.most_recent_display_time = time.time()

    def _emit_state_change_line(self, res_name):
        """ Emit line due to state of res_name having changed. """
        curr_state = self.resources[res_name]['_state']
        if curr_state == _EVAL_PENDING:
            substate = self.resources[res_name]['_subState']
        else:
            substate = ''

        self._emit_generic_line(res_name, 
                                ' ' + STATE_ABBREV_TABLE[curr_state] + ' ', 
                                substate)
        
        # Underline the row if it's a final state
        if curr_state in FINAL_STATES: 
            self._emit_generic_line(res_name, '====')
                
    def _emit_ellipses_line(self):
        """ Emit line that represents passage of time. """
        self._emit_generic_line(self, '', '')

    def _emit_table_line(self):
        """ Emit horizontal line of the needed length. """
        row = self.INDENT + '-----' * len(self.resources) + '-'
        LOGGER.info(row)

    def _emit_last_line(self):
        """ Emit last status line, checking all resources for state change. """
        state_change_count = 0
        row = self.INDENT
        for res_name in iter(sorted(self.resources)):
            curr_state = self.resources[res_name]['_state']
            if (self.resources[res_name]['_logged_state'] != curr_state):
                row = row + '| ' + STATE_ABBREV_TABLE[curr_state] + ' '
                state_change_count += 1
            else:
                row = row + '|    '
        if state_change_count > 0:
            LOGGER.info(row + '|')

    def init(self, resources):
        """ Initialize timeline.  Print header. 
            Note: shares the Resources section of a blueprint.
            """
        if len(resources) == 0:
            return

        self.resources = resources
        self.most_recent_display_time = time.time()
        
        # Header
        LOGGER.info(' ')
        LOGGER.info('Resource State Timeline')
        LOGGER.info('-----------------------')
        LOGGER.info('  State Key: ')
        LOGGER.info('     e : Evaluating')
        LOGGER.info('     ep: Evaluation pending. (See right side for pendee)')
        LOGGER.info('     es: Evaluation succeeded, creation requested')
        LOGGER.info('     EF: Evaluation failed')
        LOGGER.info('     c : Creating')
        LOGGER.info('     CF: Creation failed')
        LOGGER.info('     CS: Creation succeeded. State = READY')
        
        self._print_col_headers()

    def more_info(self, res_name):
        """ Inform timeline that the state of 'res_name' may have changed. 
            If changed, emit a row in which we show status of res_name. """
        full_state = self.resources[res_name]['_state']
        if full_state == _EVAL_PENDING:
            full_state = full_state + ' ' + self.resources[res_name]['_subState']
            
        if (self.resources[res_name]['_logged_state'] != full_state):
            self._emit_state_change_line(res_name)
            self.resources[res_name]['_logged_state'] = full_state
        if (self.refresh_frequency > 0 and 
            time.time() > self.most_recent_display_time + self.refresh_frequency):
            self._emit_ellipses_line()
            
    def finish(self):
        """ Finish timeline processing.  The state of res_name may have changed. 
            This can happen if an error occurs in res_name."""
        if len(self.resources) == 0:
            return

        self._emit_last_line()
##        if res_name != '':
##            self.more_info(res_name)
        self._emit_table_line()
        LOGGER.info(' ')

################################################################################

class CloudConnection:
    """ 
    A CloudConnection abstracts away operations on the Cloud instance.  It also
    extracts information like 'state' and 'diagnostics' in a cloud-specific manner.
    Finally, it maps cloud-specific states to those understood by the blueprint
    logic:
        - initialize(uri, creds)                              -> (nothing returned)
        - get_resource(uri, [MIME type], [request Params])    -> resource_structure
        - create_resource(container_uri, mime_type, resource) -> new_uri, status
        - get_state(uri, [MIME type])                         -> cloud_state, message/hint?
        - map_state(cloud_state)                              -> bp_state
    """
    # Note: May call functions outside of this class, in particular 'absolute_uri'

    #    State variables:
    #        cloud_uri:    
    #        user_id: 
    #        auth_token:
    #        conn:    
    def __init__(self, cloud_uri, user_id, password, validate_certificate):
        self.cloud_uri = cloud_uri
        self.user_id = user_id
        if user_id != None and password != None:
            self.auth_token = base64.encodestring(self.user_id + ':' + password)
        # todoh: won't fail here as best i can tell
        try:
            self.conn = httplib2.Http(disable_ssl_certificate_validation = not validate_certificate)
        except httplib2.SSLCertificateError as exc:
            raise_exec_error(exc.msg)
        except httplib2.SSLHandshakeError as exc:
            raise_exec_error(exc.msg)

    def _fail_if_protocol_mismatch(self, resp):
        """Check response for protocol mismatch.  If so, throw exception. 
        
        Args:
            resp -- The response received with a 2xx code.
        Returns:
            (nothing)
        Exceptions:
            BlueprintExecError: If protocol mismatch.
        Note: We want to detect a connection to an older protocol and fail.
              We do pass in the desired x-specification-version, but with the current
              implemntation, if GET passes in a value that is higher, then the
              web service uses its preferred version rather than fail. 
        """
        xs_version = None
        if 'x-specification-version' in resp:  
            xs_version = resp['x-specification-version']
        if xs_version != X_SPEC_VERSION:
            msg = ('Cloud protocol version mismatch.  Expected {0}.  Found {1}.  ' +
                   '(Is the cloud URL correct?)') 
            raise_exec_error(msg.format(X_SPEC_VERSION, xs_version))

    def _http_get_with_retries(self, abs_uri, mime_type, request_param_string):
        """Issue a GET on the absolute URI, 'accept'ing the specified mime_type, and 
        including the request_param_string.  Retry if needed.

        Args:
            abs_uri --
            mime_type -- a MIME type that uses JSON format (+json)
            request_params -- request params, as a string, prefixed with '?'
        Returns:
            The internalized JSON for that resource
        Exceptions:
            BlueprintExecError: If protocol failure, cloud/http failure, creds are bad, ...
        """
        # Issue the GET, retrying if result code is not 200 or 401
        retry_count = MAX_RETRIES
        while True:
            try:
                resp, content = self.conn.request(abs_uri + request_param_string, 'GET',
                                             headers = {'Authorization' : 'Basic ' + self.auth_token,
                                                        'x-specification-version' : X_SPEC_VERSION,
                                                        'Accept' : mime_type})
                # Temporary hack: httplib2 doesn't include 'content'
                if LOGGER.isEnabledFor(logging.DEBUG):
                    print 'Response content:'
                    print content
            except socket.error as exc:
                raise_exec_error('Http socket error: {0}'.format(exc))
            except httplib2.SSLHandshakeError as exc:
                raise_exec_error(str(exc))

            if resp.status == 401:
                msg = 'Credentials are missing or invalid. User: {0}, Content:\n{1}'
                raise_exec_error(msg.format(self.user_id, content))
            elif resp.status in [200]:  #todo: should we allow other 2xx codes?
                # Return result unless protocol version doesn't match.
                self._fail_if_protocol_mismatch(resp)
##                xs_version = None
##                if 'x-specification-version' in resp:  
##                    xs_version = resp['x-specification-version']
##                if xs_version != X_SPEC_VERSION:
##                    msg = ('Cloud protocol version mismatch.  Expected {0}.  Found {1}.  ' +
##                           '(Is the cloud URL correct?)') 
##                    raise_exec_error(msg.format(X_SPEC_VERSION, xs_version))
                try:
                    return json.loads(content)
                except ValueError as exc:
                    raise_exec_error('Unable to parse GET results from {0}\n{1}\n{2}'.format(abs_uri,
                                                                                                exc,
                                                                                                content))
            else:
                retry_count -= 1
                # Retry iff response included a retry-after and MAX_RETRIES not exceeded
                if retry_count > 0 and 'retry-after' in resp:
                    retry_after = resp['retry-after']
                    # If it is numeric and less than MAX_RETRY_PAUSE, use it.  Else use MAX_RETRY_PAUSE.
                    retry_pause_secs = MAX_RETRY_PAUSE
                    try:
                        i = int(retry_after)
                        if i < MAX_RETRY_PAUSE:
                            retry_pause_secs = i
                    except ValueError:
                        pass

                    msg = 'GET status code {0}.  Waiting {1} secs.  Remaining retries: {2}'
                    LOGGER.warning(msg.format(resp.status, retry_pause_secs, retry_count))
                    time.sleep(retry_pause_secs)
                    # Will now loop back
                else:
                    msg = 'Failure code {0} getting state for resource {1}:\n{2}\nContent:\n{3}'
                    raise_exec_error(msg.format(resp.status, abs_uri, resp, content))

    def _http_get(self, abs_uri, mime_type, request_params):
        """Issue a GET on the absolute URI, 'accept'ing the specified mime_type, and 
        including any request_params.  Retry if needed.

        Args:
            abs_uri --
            mime_type -- a MIME type that uses JSON format (+json)
            request_params -- request params, as a dict
        Returns:
            The internalized JSON for that resource
        Exceptions:
            BlueprintExecError: If protocol failure, cloud/http failure, creds are bad, ...
        """
        # TODO: Add timeout for all requests?

        # Construct request_param_string 
        request_param_string = ''
        for key in iter(request_params):
            if request_param_string == '':
                request_param_string = '?' + urllib.quote_plus(key)
            else:
                request_param_string = request_param_string + ',' + urllib.quote_plus(key)
            if request_params[key] is not None:
                request_param_string = request_param_string + '=' + urllib.quote_plus(request_params[key])

        return self._http_get_with_retries(abs_uri, mime_type, request_param_string)
                
    def get_resource(self, uri, media_type='', request_params=None):
        """ Get resource  identified by 'uri'.  Include media_type and request_params if provided. 

        Note: A cloud resource GET should support a default media_type, but some may not.
              Thus media_type should be provided if known.  Cases where it may not be known:
                - A path expr that doesn't provide such a qualifier 
        Exceptions:
            BlueprintExecError: If protocol failure, cloud/http failure, creds are bad, ...
        """
        
        request_params = request_params or {}
        return self._http_get(absolute_uri(uri, self.cloud_uri),
                              media_type,
                              request_params)

    def extract_state(self, resource):
        """ Extract state from resource.  Logic is cloud-type-specific."""
        return resource['resource_state']['state']

    def extract_diags(self, resource):
        """ 
        Extract diagnostic messages from resource.  Logic is cloud-type-specific.
        Returns a tuple: (diagnostic, details).  Details or both may be zero length but not None.
        """
        msgs = '(none available)'
        if 'messages' not in resource['resource_state']:
            return ('', '')
        try:
            msgs = resource['resource_state']['messages']
            return (msgs[0]['hint'], msgs[0]['text'])
        except KeyError:
            return ('The following information may help...\n', str(msgs))
        

    def create_resource(self, resource, res_name):
        """ 
        Create cloud resource based on its blueprint definition, 'resource'. 
        
        Args:
            resource - the blueprint resource to be created.  Evaluation should
                       have been done just prior to calling this function  
            res_name - its name, just used for diagnostics 
        Returns:
            uri of created resource 
            current cloud state
        Side effect: The 'Type' attribute of the resource is set
        Raises:
            BlueprintEvalError: if no MIME type can be discerned
            BlueprintExecError: if creation attempt failed in POST
        """
        # todo: Is there some way we can timeout if post takes too long?  Would need to a timeout param.
        res_to_post_to = resource[u'Container']
        abs_res_to_post_to = absolute_uri(res_to_post_to, self.cloud_uri)

        # Assure that resource['Type'] is set (with unabbreviated name)
        # Logic:
        #   If provided, use it (expanding any recognized abbreviation)
        #   Else consider the container...
        #     If we can GET it and it specifies a 'default_instance_media_type', use it 
        if 'Type' in resource:
            mime_type = expand_mime_abbrev(resource['Type'])
        else:
            try:
                container_res = self.get_resource(res_to_post_to)
            except (BlueprintError) as exc:
                LOGGER.debug('Error while attempting to get container to infer Type:\n{0}'.format(exc))
                msg = 'For resource {0}, no Type specified nor can one be inferred'
                raise_exec_error(msg.format(res_name))
                
            if 'default_instance_media_type' in container_res:
                mime_type = container_res['default_instance_media_type']
            else:
                msg = 'For resource {0}, no Type specified nor can one be inferred from Container'
                raise_exec_error(msg.format(res_name))
##            except (BlueprintExecError, BlueprintEvalError) as exc:
##                show_exec_error(exc)  #todo: test this case
##                raise_eval_error('No MIME type specified nor can one be inferred.')
            LOGGER.debug('No MIME type specified.  Will use: '.format(mime_type))
        resource['Type'] = mime_type

        LOGGER.debug('POST at {2} for {0} to {1} is...'.
                     format(res_name, abs_res_to_post_to,
                            time.strftime("%H:%M:%S", time.localtime())))
        #todoh: factor out calls to request.  Also, are all the exceptions needed?  Also
        #       use this syntax when applicable: except (ex1, ex2) as ex
        try:
            resp, content = self.conn.request(abs_res_to_post_to, 'POST',
                                         json.dumps(resource['Properties']),
                                         headers = {'Authorization' : 'Basic ' + self.auth_token,
                                                    'Accept' : mime_type,
                                                    'x-specification-version' : X_SPEC_VERSION,
                                                    'Content-Type' : mime_type})
        except httplib2.ServerNotFoundError as exc:
            raise_exec_error('Server not found for {0}: {1}'.format(res_name, exc))
        except ssl.SSLError as exc:
            # Timeouts are surfaced as SSLErrors.  
            #todo: no longer needed?
            raise_exec_error('HTTP error for {0}: {1}'.format(res_name, exc))
        except socket.error as exc:
            raise_exec_error('Http socket error: {0}'.format(exc))
        except httplib2.SSLHandshakeError as exc:
            raise_exec_error(str(exc))

        if resp.status in [200, 201, 202]:  # Jack: "Will only get 200"
            json_content = yaml.safe_load(content)
            return (json_content[u'uri'],
                    json_content['resource_state']['state'] )
        else:
            raise_exec_error('Failure creating resource {0}: HTTP Code={1}\n{2}'.format(res_name, resp.status, content))
        

    def map_state(self, cloud_state, resource_name):
        """ Return the blueprint state that corresponds to cloud resource's real state. """
        if cloud_state == _CLOUD_READY:
            state = _READY
        elif cloud_state in [_CLOUD_INITIATED, _CLOUD_CREATING, _CLOUD_CREATED]:
            state = _CREATING
        elif cloud_state in [_CLOUD_EXCEPTION, _CLOUD_UNKNOWN]:
            state = _CREATE_FAILED
        else:
            LOGGER.critical('Resource {1} unexpected cloud state: {0}'.
                            format(cloud_state, resource_name))
            state = _CREATE_FAILED
        return state        

################################################################################

# Next two classes are used for generating the graphics/chart

# Colors
##CLR_DEFAULT = 'bisque'  # was khaki1
##CLR_INPUT = 'darkseagreen1' # 'goldenrod'  
##CLR_OUTPUT = 'darkolivegreen2'
##CLR_EXTERNAL_RES = 'burlywood3'
##CLR_READY_STATUS = 'darkolivegreen2'
##CLR_NOT_READY_STATUS = 'coral1'

CLR_DEFAULT = '#fff9b8'  # 3
CLR_INPUT = '#e3cb96'    # 4
CLR_OUTPUT = '#e3cb96'   # 4
CLR_EXTERNAL_RES = '#cae3f3' # 5
CLR_TYPE = '#ccccb9'
CLR_READY_STATUS = '#4fb54a'
CLR_NOT_READY_STATUS = '#dd2c2e'

##CLR_DEFAULT = '#d9ae6e'  # was khaki1
##CLR_INPUT = '#d98f6d'  
##CLR_OUTPUT = '#d98f6d'
##CLR_EXTERNAL_RES = '#476c91'
##CLR_TYPE = '#ccccb9'
##CLR_READY_STATUS = '#4fb54a'
##CLR_NOT_READY_STATUS = '#dd2c2e'
##
class ResourceRows:
    """The resource rows to be added to a resource label.
    
       The primary use is for Properties, but also used for Container.
       Each row has a label and value.  The label can be a name or '- ', in the 
       case of list elements.  The value may be a scalar.  If a function call,
       the value is left blank and the indented rows that follow represent a function 
       call.
    """
    
    def __init__(self, res_name):
        self.rows = ''
        self.res_name = res_name  #todoh: not used.  Delete it.


    INDENT_STRING = '&nbsp;' + '&nbsp;' + '&nbsp;'
    def append_row(self, label, value, level, bgcolor=CLR_DEFAULT, port=''): 
        # Note that prop_name may be '- ', indicating a list element
        if port == '':
            port = label
        # Label construction: Add a colon, or make it a bullet
        if label == '':
            label = BULLET_STRING
        else:
            label = label.strip() + ': '
        if isinstance(value, ResourceRows):
            print 'CRITICAL: value = ', str(value)  #todoh: not needed?
        else:
            self.rows = (self.rows + '<TR><TD BGCOLOR="' + bgcolor + '" align="left" port="' + port + '">' + 
                         '<b>' + self.INDENT_STRING * level + label + '</b>' + 
                         str(value) + '</TD></TR> ')


    def append_rows(self, new_rows):
        if isinstance(new_rows, ResourceRows):
            self.rows = self.rows + new_rows.rows
        else:
            print 'CRITICAL: new_rows type = ', str(type(new_rows))

class ResourceLabel:
    """The resource label will be added to a resource node.
    
       To construct a complete resource label
         - Call init_name_type()
         - Construct and populate ResourceRows object 
         - Append it 
         - Call finish()       
    """
    
    def __init__(self):
        self.name = ''
        self.level = 0
        self.value = ''

    def init_name_type(self, name, type):
        self.name = name
        if type == '':
            type_display = '&nbsp;'
        else:
            type_display = 'Type: ' + type
        self.value = ('<' + 
             '<TABLE TITLE="{0}" BORDER="1" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">'.format(name) + 
             '  <TR><TD><b>' + name + '</b></TD></TR> ' + 
             '  <TR><TD BGCOLOR="' + CLR_TYPE + '" align="left">' + type_display + '</TD></TR>') 

    def append_rows(self, new_rows):
        if isinstance(new_rows, ResourceRows):
            self.value = self.value + new_rows.rows
        else:
            print 'CRITICAL: new_rows of resLabel type = ', str(type(new_rows))

    def finish(self):
        self.value = self.value + '</TABLE> >'


################################################################################

class Blueprint:
    """ 
    A blueprint can be initialized and instantiated (i.e. an instance of it
        created).  

    The only methods that should be considered public are:
        initialize
        instantiate_bp
        (some test cases, call _initialize() and pass in the text of a blueprint)
    Notes:
        - Runtime state is represented in this class.  To instantiate a blueprint 
          a second time, you must re-initialize.
    """

    # TODOL: Only use unicode?
    
    # Expression evaluation
    #
    # Much of blueprint processing is expression evaluation.  To instantiate a resource
    # it evaluates the Container expression.  To create a resource it posts a JSON document
    # to the container and this document must be evaluated first.
    #
    # Evaluation is done by calling the 'evaluate' function.  Input is a 'parsed'
    # representation of a JSON/YAML document, i.e. its Python equivalent.  The evaluation  
    # operation replaces each function call and macro invocation with the resulting value.  
    # (Both calls are identified by a name that begins with 'f_'.)  Calls can be nested.
    #
    # Evaluation may fail, e.g. throw a BlueprintEvalError exception when it refers
    # to something that doesn't exist.  Evaluation may also determine that it cannot
    # proceed until another resource is created, in which case it throws a BlueprintEvalPending
    # exception.
    #
        
    # Simulation mode
    #
    # Simulation mode is used for testing and debugging internally and also by end users.
    # To use simulation mode, specify no cloud URI (the -c command line option).  
    #
    # Simulation mode differs from normal mode in these ways:
    #  . Getting the state of a resource is based on simulation settings:
    #    - delay 
    #    - result
    #    (See get_cloud_res_state.)
    #  . Post to create a resources: returns URI of type SimValue(tbd).  This includes
    #    the resource name, which will be used for getting simulated status and attribute 
    #    values
    #  . Get resource status(URI): uses the resource name to get simulation info,
    #                              which determines delay and final status
    #
    # Expression evaluation is somewhat different in sim mode:
    #  . Expressions are evaluated normally if the context (document) is not a sim 
    #    value.  Expression evaluation normally begins with a blueprint, e.g. 
    #           Resources.myRes.blah 
    #  . After several operations, it may attempt to access the cloud, e.g.
    #           Resources.myRes._uri-> 
    #    At that point, the context (document) becomes a SimValue (call it 'xx').
    #      - With most operations, just 'append' them to the SimValue, e.g. for xx.name 
    #        and xx[n]... append '.name' or '[n]' to xx
    #      - URI traversal is special:
    #        The _uri attribute of every resource definition is initiatlized to
    #        "Resource <res name>>".  If the traversal operator is applied to such a
    #        value, the result is a SimValue whose value is set to "URI of <resname>"
    #        (Don't traverse and continue unless the resource state is READY)

    # TODO: Style: Should all literals be replaced by constants, e.g. ...['Value']?

    ITERATION_DELAY = 2                 # Seconds
    DEFAULT_SIMULATION_DELAY = 2        # Seconds
    INITIAL_URI_VALUE_PREFIX = 'Resource '
    
    # Dispatch table used for intrinsic functions and macros
    # First 2 values are Arg count list min and max.
    # Third value, if present, is the definition of the macro.  This is added by
    #    incorporate_oracle_macros.
    #
    # To add an instrinsic function:
    #   . Add entry to this table
    #   . Add a function named "fn_<Name>"
    DISPATCH_TABLE = {'break': [1, 2],
                      'concat': [2, 99],
                      'deref': [1, 2],
                      'deref_diag': [2, 3],  # Last arg is a diagnostic
                      'eval': [1, 1],
                      'failIf': [2, 3],
                      'findAll': [2, 2],
                      'findOne': [2, 3],
                      'getReadyResource': [1, 1],
                      'path': [1, 1],
##                      'path': [1, 2],
                      'pathc': [1, 2],
                      'path_diag': [2, 2],   # Last arg is a diagnostic
                      'pathc_diag': [2, 3],  # Last arg is a diagnostic
                      'print': [1, 2],
                      'replace': [3, 3],
                      'replacePathExprs': [1, 1],
                      'waitForOne': [2, 4],
                      'EQ': [2, 2],
                      'IN': [2, 99],
                      'NE': [2, 2],
                      'NOT': [1, 1],
                      }
    LEGAL_SECTIONS = ['Inputs', 'Macros', 'Data', 'Resources', 'Outputs']  # Legal for user to provide
    ADDED_SECTIONS = ['Cloud', 'Info']                                     # Added by us
    ALLOWED_RESOURCE_SECTIONS = ['Container', 'Type', 'Properties', 'Simulation']
    ALLOWED_INPUT_ATTRS = ['Prompt', 'DefaultValue', 'Type', 'Sensitive',
                           'Description', 'Order']
    ALLOWED_INPUT_TYPES = ['String', 'Number']
    ALLOWED_INPUT_SENSITIVITIES = ['True', 'False']
    
    def __init__(self):
        self.bp_parsed = ''
        self.cloud_connection = None
        self.context = []            # Engine context: Stack of evaluation contexts
        
        # Timing info:
        #   . start_time: time when we began
        #   . pause_secs: number of seconds spent paused, e.g. in break point or interactive input
        #                 The above two times are used with 'timeout' param to know when to quit
        #
        self.timing = dict()
        self.timing['start_time'] = time.time()
        self.timing['pause_secs'] = 0
        
# ==============================================================================
# Section: Utility functions such as dump and is_in_sim_mode

    def is_in_sim_mode(self):
        """ Return Boolean for whether blueprint is in simulation mode.  
            (Based on whether there is a real cloud connection.)
        """

        return self.cloud_connection.cloud_uri == ''


# ==============================================================================
# Section: Cloud operations, which in turn use CloudConnection (unless simulated)

    def post_simulated(self, res_name):
        """Simulate creation of a resource using its definition.

        Args:
            res_name: resource name.  
        Returns:
            URI SimValue (for res_name)
            Cloud resource state (_CLOUD_CREATING)
        Raises:
            none
        Notes:
            Sets the resource's _finish_after value, so that subsequent GETs know
            when to return a completion status.
        """
        resource = self.bp_parsed['Resources'][res_name]
        res_to_post_to = resource[u'Container']
        resulting_uri = SimValue()
        resulting_uri.set_for_resource_uri(res_name)
        LOGGER.debug('Sim-POSTing for resource {0} to {1} is...'.format(res_name,
                                                                        res_to_post_to))
        LOGGER.debug('POST results: URI={0}. (Its status will be {2} in {1} secs.)'.
                     format('{{uri for {0}}}'.format(res_name),
                            resource['Simulation']['delay'],
                            resource['Simulation']['result']))
        resource['Simulation']['_finish_after'] = (time.time() + resource['Simulation']['delay'])
        return resulting_uri, _CLOUD_CREATING


    def post_for_create(self, res_name, remaining_time):
        """Initiate creation of a resource (simulated or actual) using its definition.

        Args:
            res_name: resource name.  Used to access the resource's URI to post
                     to and the MIME doc to post.  The state of the resource
                     must be 'evaluated'.
            remaining_time -- # of seconds to allow; Not currently used
        Returns:
            URI returned as a result of posting (may be a SimValue)
            Resource state (one of the cloud resource states)
        Raises:
            BlueprintEvalError: if no MIME type can be discerned
            BlueprintExecError: if creation attempt failed in POST (e.g. POST doesn't return 200)
        Notes:
            If in simulation mode, no exceptions are raised.
        """
        self.bp_parsed['Resources'][res_name]['_create_start_time'] = time.time()
        if self.is_in_sim_mode():
            return self.post_simulated(res_name) # resulting_uri, _CLOUD_CREATING
        else:
            return self.cloud_connection.create_resource(self.bp_parsed['Resources'][res_name],
                                                         res_name)


    def get_cloud_res_state_simulated(self, resource_name):
        """Get current status for a simulated cloud resource.  Must first have attempted to create it.

        Args:
            resource_name --
        Returns:
            curr_cloud_state: _CLOUD_CREATING, _CLOUD_READY, or _CLOUD_EXCEPTION
            diag:             diagnostic or ''
        Notes: 
            - Returns the cloud resource status, not our (normalized) resource state.
            - Return _CLOUD_CREATING until the simulation delay time is exceeded, 
              then return _CLOUD_READY, or _CLOUD_EXCEPTION per the specified sim value,
              's' or 'f'.
        """
        resource = self.bp_parsed['Resources'][resource_name]
        diag = ''

        res_sim_data = resource['Simulation']
        if '_finish_after' not in res_sim_data:  # First time?
            # Should never happen.  Means we never attempted to create the resource. 
            LOGGER.critical('No finish_after time set prior to GET')  
            res_sim_data['_finish_after'] = time.time() + res_sim_data['delay']

        if time.time() < res_sim_data['_finish_after']:
            curr_cloud_state = _CLOUD_CREATING
        elif res_sim_data['result'] == 's':
            curr_cloud_state = _CLOUD_READY
            resource['_create_end_time'] = time.time()
        else:
            curr_cloud_state = _CLOUD_EXCEPTION
            diag = 'Simulated diagnostic for ' + resource_name
        return curr_cloud_state, diag

    def get_cloud_res_state(self, resource_name):
        """Get current status for a cloud resource.  Must first have attempted to create it.

        Note: returns the cloud resource status, not our (normalized) resource state.

        Args:
            resource_name --
        Returns:
            curr_cloud_state: 
            diag:             diagnostic or ''
            details:          diagnostic details or ''
        Raises:
            BlueprintExecError: If protocol failure, cloud/http failure,  ...
        """
        if self.is_in_sim_mode():
            curr_cloud_state, diag = self.get_cloud_res_state_simulated(resource_name)
            details = ''
        else:
            resource = self.bp_parsed['Resources'][resource_name]
            res_uri = resource['_uri']
            if not is_uri_set(res_uri):
##            if res_uri.find(self.INITIAL_URI_VALUE_PREFIX) == 0:
                # Should never happen.  Means we never POSTed to create the resource. 
                LOGGER.critical('No URI for resource {0}'.format(resource_name))  
            res_type = resource['Type']
            cloud_res = self.cloud_connection.get_resource(res_uri, res_type) 
            curr_cloud_state = self.cloud_connection.extract_state(cloud_res)
            diag, details = self.cloud_connection.extract_diags(cloud_res)
            LOGGER.debug('Resource {0} state: {1}'.format(resource_name, curr_cloud_state))
        
        return curr_cloud_state, diag, details


# ==============================================================================
# Section: Expression evaluation, e.g. find_one, fail_if, and get_ready_resource

    def find_one(self, array, predicate, wait_flag = False, 
                 error_message = '', wait_message = ''):
        """ Return first array element for which predicate is satisfied

        Args:
            array  --    document to operate on.  Coerced to an array.
            predicate -- 
            wait_flag -- If true, throw a 'pending' exception instead of failing
            error_message -- Failure message to override the default message
            wait_message --  Wait message to override the default message.
                             Ignored if wait_flag is false.
        Returns:
            Array element for which predicate is satisfied
            If simulation mode, return SimValue with appended info.
        Description:
            For each value in array, set it as 'context'.  Then evaluate 'predicate'.
            Return first value that satisfies 'predicate'.
        Notes: 
            - Same as f_findAll(jsonArray, predicate), only it stops and returns as soon as
              a matching element is found.  It returns an element, rather than an array.
            - When wait_flag is true, we are executing f_waitForOne.
        Exceptions: (also exceptions from 'evaluate')
            BlueprintEvalError: If the predicate is bad
            BlueprintEvalPending: If evaluation should be retried later (due to inner expr)
        """
        if isinstance(array, SimValue):
            array.append('f_findOne(' + str(predicate) + ')')
            return array
        array = coerce_to_array(array)
        for val in array:
            self.context.append(val)
            result = self.evaluate(predicate)
            if not isinstance(result, bool):
                message = 'findOne: Predicate "{0}" yields value "{1}": not Boolean'
                raise_either_eval_diag(message.format(predicate, result),
                                       error_message)
##                raise_eval_error(message.format(predicate, result))
            self.context.pop()
            if result:
                return val
        if wait_flag:
            if wait_message == '':
                raise_pending('f_waitForOne with predicate {0}'.format(predicate))
            else:
                raise_pending(wait_message)
        else:
            raise_either_eval_diag('find_one for predicate {0} failed'.format(predicate),
                                   error_message)


    def find_all(self, array, predicate, error_message = ''):
        """ Return all array elements for which predicate is satisfied

        Args:
            array  --    document to operate on.  Coerced to an array.
            predicate --
        Returns:
            All array elements that satisfy the predicate, as an array
        Description:
            For each value in array, set it as 'context'.  Then evaluate 'predicate'.
            Return all values that satisfy 'predicate'.
        Exceptions: (also exceptions from 'evaluate')
            BlueprintEvalError: If no element satisfies the predicate or the predicate is bad
            BlueprintEvalPending: If evaluation should be retried later (due to inner expr)
        """
        if isinstance(array, SimValue):
            array.append('f_findAll(' + str(predicate) + ')')
            return array
        array = coerce_to_array(array)
        vals_to_return = []
        for val in array:
            self.context.append(val)
            result = self.evaluate(predicate)
            if not isinstance(result, bool):
                message = 'findAll: Predicate "{0}" yields value "{1}": not Boolean'
                raise_either_eval_diag(message.format(predicate, result), error_message)
            self.context.pop()
            if result:
                vals_to_return.append(val)
        if len(vals_to_return) == 0:
            raise_either_eval_diag('find_all for predicate {0} failed'.format(predicate),
                                   error_message)
##            raise_eval_error("find_all for predicate {0} failed".format(predicate))
        else:
            return vals_to_return

    # TODO: Add , wait_flag = False??
    def fail_if(self, array, predicate, error_message = ''):
        """ Fail if predicate is satisfied for any array element.

        Args:
            array  --
            predicate --
            error_message -- Failure message to override the default message
        Returns:
            the value of jsonArray, coerced to an array.  But fail, if
            the predicate is satisfied for any element
        Description:
            For each value in array, set it as 'context'.  Then evaluate 'predicate'.
            Fail as soon as a value satisfies 'predicate'.
        Exceptions: (also exceptions from 'evaluate')
            BlueprintEvalError: If the predicate is satisfied or is bad
            BlueprintEvalPending: If evaluation should be retried later (due to inner expr)
        """
        if isinstance(array, SimValue):
            return array
        array = coerce_to_array(array)
        for val in array:
            self.context.append(val)
            result = self.evaluate(predicate)
            if not isinstance(result, bool):
                message = 'failIf: Predicate value {0} is not Boolean'
                raise_either_eval_diag(message.format(result), error_message)
            self.context.pop()
            if result:
                message = 'fail_if: Value {0} satisfied predicate {1}'
                raise_either_eval_diag(message.format(val, predicate), error_message)
        return array

# f_getResourceAttr is currently implemented as a macro.  Leave the following code
# commented-out for now, in case we have to revert.

##    def getResourceAttr(self, res_name, attr_name):
##        """ GET the value of the resource's attribute.
##
##        Args:
##            res_name  --
##            attr_name --
##
##        Returns:
##            Value of the expr
##
##        Exceptions:
##            BlueprintEvalError: If the resource doesn't exist
##            BlueprintEvalPending: If evaluation should be retried later
##
##        """
##        print '==== getResourceAttr: {0}'.format(res_name + '.' + attr_name)
##        try:
##            resource = self.bp_parsed['Resources'][res_name]
##        except KeyError:
##            raise_eval_error("Resource " + res_name + " not present")
##
##        try:
##            res_uri = resource['_uri']
##        except KeyError:
##            raise_eval_error("URI for resource " + res_name + " not present")
##        if resource['_state'] in (Blueprint._RES_READY):
##            if (isinstance(res_uri, SimValue) or
##                res_uri.startswith(self._SIMULATION_PREFIX)):
##                return '>>>Attr_' + attr_name + '_from_' + res_name
##            else:
##                json_content = self.http_get_res(res_name)  # May raise 'failure'.  Throws to caller.
##                try:
##                    return json_content[attr_name]
##                except KeyError as ex:
##                    LOGGER.error('Attr {0} not found in resource {1}'.format(attr_name, res_name))
##                    raise_eval_error(ex.msg)
##        elif resource['_state'] in (Blueprint._RES_EVAL_FAILED, Blueprint._RES_CREATE_FAILED):
##            raise_eval_error('getResourceAttr for ' +
##                                '{0} failed because its state is {1}: '.format(res_name,
##                                                                               resource['_state']))
##        else:
##            raise_pending('{0}: ResourceAttr({1})'.format(res_name, attr_name))

    def get_ready_resource(self, res_name):
        """ GET the entire value of the resource, if ready.

        Args:
            res_name - name of resource
        Returns:
            Value of cloud resource, if READY.
        Exceptions:
            BlueprintEvalError: If the resource doesn't exist
            BlueprintEvalPending: If evaluation should be retried later
            BlueprintExecError: Cloud/http error
        """
        try:
            resource = self.bp_parsed['Resources'][res_name]
        except KeyError:
            raise_eval_error("Resource " + res_name + " not present")
        
        res_uri = resource['_uri']
        if resource['_state'] == _READY:
            if isinstance(res_uri, SimValue):
                res_uri.append('->')
                return res_uri
            else:
                res_type = resource['Type']
                return self.cloud_connection.get_resource(res_uri, res_type)  # May raise blueprint error. 
        elif resource['_state'] in [_EVAL_FAILED, _POST_FAILED]:
            # Won't happen during resource eval because failure should terminate further
            # processing.  But the Outputs section may attempt to access failed resources.
##            msg = 'Resource {0} in state {1}; Should have terminated before getReadyResource call'
##            LOGGER.critical(msg.format(res_name, resource['_state']))
            msg = 'Resource {0} not ready.  State is {1}: '.format(res_name,
                                                                   resource['_state'])
            raise_exec_error(msg)  
##            raise_eval_error(msg)  # todo: remove soon; Replaced by above on aug 3     
        else:
            raise_pending('Awaiting creation of {0}'.format(res_name))

# ==============================================================================
# Section: Path expression parsing/evaluation
   
    def path_eval_arrow_qualifiers(self, tokens, diag):
        """ Evaluate the optional traversal qualifiers, enclosed in square brackets.
        
            Return a dict representation of the qualifiers. 
            Qualifiers are either of the form 
               1) <identifier> [= <string>] 
               2) <string>
            Case 1: Dict entry will use <identifier> with value of <string> or None.
            Case 2: Dict entry will use identifier of ''.
            If a duplicate identifer value is encountered, diagnose it.
            Returns a tuple: (qualifiers, tokens)."""
        # Note: The following was crafted from a state-transition diagram.  Here's 
        #       a textual representation:
        #
        # State               Meaning
        # -----               -------
        # expectingQualifier: after the '[' and after ',' we expect a new qualifier
        # named:              the name has been seen
        # valued:             the value has been seen
        # expectingValue:     an '=' has been seen, so we require a value
        # leaving:            a ']' has been seen, so we save the last qualifier and finish
         
        qualifiers = {} 
        if len(tokens) == 0 or tokens[len(tokens) - 1][0] != 'open_bracket':
            return (qualifiers, tokens)   # No arrow qualifiers
            
        tokens.pop()  # Discard '['
        
        state = 'expectingQualifier'
        while True:            
            # Expecting an entry or ']'
            tok = pop_or_diagnose(tokens, 'Expected "]" or traversal qualifer', diag)
            if state == 'expectingQualifier':
                name = ''
                value = None
                if tok[0] == 'close_bracket':
                    state = 'leaving'
                elif tok[0] == 'identifier':
                    name = tok[1]
                    state = 'named'
                elif tok[0] == 'string':
                    value = tok[1]
                    state = 'valued'
                else:
                    msg = 'Expected <identifier>, <string>, or "]", not {0}'
                    raise_either_eval_diag(msg.format(tok[0]), diag)
            elif state == 'named':
                if tok[0] == 'close_bracket':
                    state = 'leaving'
                elif tok[0] == 'comma':
                    qualifiers[name] = value
                    state = 'expectingQualifier'
                elif tok[0] == 'equals':
                    state = 'expectingValue'
                else:
                    msg = 'Expected "=", ",", or "]", not {0}'
                    raise_either_eval_diag(msg.format(tok[0]), diag)
            elif state == 'expectingValue':
                if tok[0] == 'string':
                    value = tok[1]
                    state = 'valued'
                else:
                    msg = 'Expected <string>, not {0}'
                    raise_either_eval_diag(msg.format(tok[0]), diag)
            elif state == 'valued':
                if tok[0] == 'close_bracket':
                    state = 'leaving'
                elif tok[0] == 'comma':
                    qualifiers[name] = value
                    state = 'expectingQualifier'
                else:
                    msg = 'Expected "," or "]", not {0}'
                    raise_either_eval_diag(msg.format(tok[0]), diag)

            if state == 'leaving':
                if name != '' or value != None:
                    qualifiers[name] = value
                return (qualifiers, tokens)
    
    def path_eval_arrow_oper(self, doc, tokens, diag):
        """ Evaluate the arrow operator of tokens.  Return new doc and remaining tokens."""

        # Get optional qualifers 
        qualifiers, tokens = self.path_eval_arrow_qualifiers(tokens, diag)
        
        if isinstance(doc, SimValue):
            res_name = doc.get_for_resource_uri()
            if res_name != '':
                curr_cloud_state, diag = self.get_cloud_res_state_simulated(res_name)
                if curr_cloud_state == _CLOUD_CREATING:
                    raise_pending('Awaiting creation of ' + res_name)
                elif curr_cloud_state == _CLOUD_EXCEPTION:
                    self.bp_parsed['Resources'][res_name]['_state'] = _CREATE_FAILED
                    raise_either_eval_diag('Creation of resource ' + 
                                           '{0} failed'.format(res_name), 
                                           diag)
##            if res_name != '':
##                # doc is for a resource's _uri; simulate the delay and status set
##                resource = self.bp_parsed['Resources'][res_name]
##                # TODO: This duplicates logic elsewhere.  We should never go Pending here, right?  
##                if ('_finish_after' not in resource['Simulation'] or
##                    time.time() < resource['Simulation']['_finish_after']):
##                    raise_pending('Awaiting creation of ' + res_name)
##                elif resource['Simulation']['result'] == 's':
##                    pass
##                else:
##                    resource['_state'] = _CREATE_FAILED
##                    raise_either_eval_diag('Creation of resource ' + 
##                                           '{0} failed'.format(res_name), 
##                                           diag)
##            doc = copy.deepcopy(doc)  #todo: not always needed?  But is needed at least for _uri
            doc.append('->')
        else:
            doc = coerce_to_string(doc)
            if not is_uri_set(doc):   # Resource URI has not been set to the result of POST
##            if doc.find(self.INITIAL_URI_VALUE_PREFIX) == 0:  # Resource URI has not been set to result of POST
                raise_pending('Waiting for {0}, creation not yet requested'.format(doc))
            doc = absolute_uri(doc, self.cloud_connection.cloud_uri)
            
            mime_type = ''
            if '' in qualifiers:
                mime_type = qualifiers['']
                del qualifiers['']           
            try:
                doc = self.cloud_connection.get_resource(doc, 
                                                         expand_mime_abbrev(mime_type), 
                                                         qualifiers)  # qualifiers are request_params
            except BlueprintEvalError:
                raise_either_eval_diag('Failed on GET of {0}'.format(doc), diag)
        return doc, tokens

    def path_eval_dot_oper(self, doc, tokens, diag):
        """ 
        Evaluate the dot operator of tokens.  Return new doc and remaining tokens.
        (If doc is a SimValue, just append .<id>) 
        """
        if len(tokens) == 0:
            raise_either_eval_diag('Dot operator must be followed by an identifier', diag)
        tok = tokens.pop()
        if isinstance(doc, SimValue):
##            doc = copy.deepcopy(doc)   #todo: not always needed?  But is needed at least for _uri
            doc.append('.' + tok[1])
        else:
            if type(doc) != dict:
                raise_either_eval_diag('Dot operator must be applied to a dict,' +
                                       ' not {0}'.format(type(doc)), diag)
            try:
                doc = doc[tok[1]]
            except KeyError:
                default_msg = ('Attribute name not found for dot operator: "{0}", ' +
                               'when applied to document that begins with: "{1}".')
                raise_either_eval_diag(default_msg.format(tok[1], str(doc)[0:80]),
                                            diag)
        return doc, tokens

    def path_eval_bracket_oper(self, doc, tokens, diag):
        """ 
        Evaluate the remainder of the bracket operator.  
        (The '[' has already been popped from 'tokens'.  Process the <integer> and ']'
        that remain.)
        
        Return new doc and remaining tokens.
        """
        tok = pop_or_diagnose(tokens, 'Expected "]" or traversal qualifer', diag)
        if tok[0] != 'integer':
            raise_either_eval_diag('Expecting integer but found "{0}"'.format(tok[1]), diag)
        index = int(tok[1])

        tok = pop_or_diagnose(tokens, 'Expected "]"', diag)
        if tok[0] != 'close_bracket':
            raise_either_eval_diag('Expecting "]" but found : {0}'.format(tok[1]), diag)

        if isinstance(doc, SimValue):
##            doc = copy.deepcopy(doc)   #todo: not always needed?  But is needed at least for _uri
            doc.append('[' + str(index) + ']')
        else:
            if type(doc) != list:
                msg = 'Bracket operator must be applied to a list, not {0}'
                raise_either_eval_diag(msg.format(type(doc)), diag)
            try:
                doc = doc[index]
            except IndexError:
                raise_either_eval_diag('Element with index {0} not found'.format(index), diag)
        return doc, tokens

    def path_eval_dollar(self, tokens, diag):
        """ 
        Evaluate the dollar operator of tokens.  Return new doc and remaining tokens.
        """
        tok = pop_or_diagnose(tokens, 'Expected string after "$"', diag)
        if tok[0] != 'string':
            raise_either_eval_diag('Expecting string after "$" but found "{0}"'.format(tok[0]), 
                                   diag)
        doc = tok[1]
        return doc, tokens
        
    def path_eval(self, doc, path_expr, diag=''):
        """ Evaluate path_expr against doc, returning result

        Args:
            doc  --       Python representation of some YAML 
            path_expr -- An expression that describes what to extract
            diag --       Diagnostic, if provided and if eval fails
        Returns:
            The result.
        Exceptions:
            BlueprintEvalError
        Note:
            Is it sufficient to assume member names are alphanumeric?  May need
            to use reg exprs, allow periods to be escaped, etc.
        """
        tokens = tokenize(path_expr)
        tokens.reverse()  # So we can use it as a stack        
        while len(tokens) > 0:
            # The dot operator is optional, e.g. in path expressions like these, '*' can be '.' or '':
            #    *Cloud
            #    Cloud.uri->*description
            if tokens[len(tokens) - 1][0] in ['identifier']:
                tokens.append(('dot', '.'))
            tok = tokens.pop()
            try:
                if tok[0] == 'dot':
                    doc, tokens = self.path_eval_dot_oper(doc, tokens, diag)
                elif tok[0] == 'arrow':
                    doc, tokens = self.path_eval_arrow_oper(doc, tokens, diag)
                elif tok[0] == 'open_bracket':
                    doc, tokens = self.path_eval_bracket_oper(doc, tokens, diag)                
                elif tok[0] == 'dollar':
                    doc, tokens = self.path_eval_dollar(tokens, diag)                
                else:
                    raise_either_eval_diag('Operator not supported: ' + tok[0], diag)
            except BlueprintEvalError as exc:
                exc.push_expr(path_expr)
                reraise_eval_error(exc)
        return doc

    def path_eval_of_context(self, path_expr, diag=''):
        """ Extract subset from context based on path_expr"""
        return self.path_eval(self.context[len(self.context) - 1],  # Top of stack
                              path_expr, diag)

    def path_eval_of_bp(self, path_expr, diag=''):
        """ Extract subset from context based on path_expr"""
        return self.path_eval(self.bp_parsed,
                              path_expr, diag)

# ==============================================================================
# Section: General expression/function evaluation, including fn_* functions

    def evaluate_function(self, function_name, arg_list):
        """ Evaluate the function (or macro), using a list of params.

        Args:
            function_name -- name of a function (beginning with '!' or 'f_') or
                             name of a macro (same prefix)
            arg_list      -- list of parameters (must be a Python 'list')
        Returns:
            Function / macro evaluation results
        Exceptions:
            BlueprintEvalError: If the expression is not valid
            BlueprintEvalPending: If evaluation should be retried later
        """

        # Convert unicode to ascii
        internal_name = function_name.encode()
##        print '=== function name:', internal_name
##        print '=== function name:', self.DISPATCH_TABLE

        if internal_name not in self.DISPATCH_TABLE:
            raise_eval_error('No such function or macro: {0}'.format(function_name))
        if type(arg_list) != list:
            raise_eval_error('Function/macro args must be passed via list, not {0}'.format(type(arg_list)))
        param_cnt_min = self.DISPATCH_TABLE[internal_name][0]
        param_cnt_max = self.DISPATCH_TABLE[internal_name][1]
        if len(arg_list) < param_cnt_min or len(arg_list) > param_cnt_max:
            if param_cnt_min == param_cnt_max:
                diag = 'Function/macro {0} requires parameter count of {1}, not {3}'
            else:
                diag = 'Function/macro {0} requires parameter count between {1} and {2}, not {3}'
            raise_eval_error(diag.format(function_name, param_cnt_min,
                                         param_cnt_max, len(arg_list)))
        # Call the requested intrinsic function or macro
        if len(self.DISPATCH_TABLE[internal_name]) < 3:
            return getattr(Blueprint, 'fn_' + internal_name)(self, arg_list)
        else:
            macdef = self.DISPATCH_TABLE[internal_name][2]
            # TODO: Need escape conventions, if one really needs arg_ in a string?
            macro_w_args = copy.deepcopy(macdef)
            for i, val in enumerate(arg_list):
                replace_in_dicts_and_lists(macro_w_args, 'arg_' + str(i + 1), val)
            return self.evaluate(macro_w_args)

   
    def evaluate(self, expr):
        """Evaluate 'expr'.

        Args:
            expr -- expression to be evaluated
        Returns:
            Value of the evaluated expr
        Exceptions:
            BlueprintEvalError: If the expression is not valid
            BlueprintEvalPending: If evaluation should be retried later
        """
        try:
            if type(expr) is dict:
                for key in iter(expr):
                    if key[0] == u'!':    # If a function, it will evaluate its params'
                        LOGGER.warning('Deprecated: Use "f_", not "!" to identify a function: {0}'.format(key))
                        expr = self.evaluate_function(key[1:], expr[key])
                    elif key[0:2] == u'f_':
                        expr = self.evaluate_function(key[2:], expr[key])
                    else:
                        expr[key] = self.evaluate(expr[key])   # Evaluate the right-side value
            elif type(expr) is list:
                for i in range(len(expr)):
                    expr[i] = self.evaluate(expr[i])
                    # for each element, replace with evaluation of it
            elif type(expr) in LEGAL_SCALARS:
                pass
            elif expr is None:
##                expr = ''
                pass
            elif isinstance(expr, SimValue):
                pass
            else:
                LOGGER.critical('Bad expr type: ' + str(type(expr)))
##                raise_eval_error('Bad expr type: ' + str(type(expr)))
            return expr
        except BlueprintEvalError as exc:
            exc.push_expr(expr)
            reraise_eval_error(exc)
##            raise_eval_error('{0}\n'.format(expr) + exc.msg)

#####
#
# Intrinsic functions are all dispatched to the following
# There are several ways to handle intrinsics
#  - If short, just do it
#  - If intrinsic is essentially a macro, i.e. semantics can be defined as an
#    expression, construct and evaluate the expression
#  - Call a function to do the work (e.g. path_eval_of_context)

    def fn_break(self, arg_list):
        """ Print optional breakpoint message and drop into 'debugger'.  Return 1st arg.  """
        if len(arg_list) > 1:
            pause_text = '>>> Breakpoint: {0}'.format(self.evaluate(arg_list[1]))
        else:
            pause_text = '>>> Breakpoint'
        self.enter_debugger(pause_text)
        return self.evaluate(arg_list[0])  # We eval the arg after the break

    def fn_concat(self, arg_list):
        """ Concatenate string values of all args.  """
        for i in range(0, len(arg_list)):
            arg_list[i] = str(self.evaluate(arg_list[i]))
        return ''.join(arg_list)

    def fn_deref(self, arg_list):
        """ Equivalent to fn_path.  Deprecated.  """
        LOGGER.warning('Deprecated: Use "f_path", not "f_deref"')
        return self.fn_path(arg_list)

    def fn_deref_diag(self, arg_list):
        """ Equivalent to fn_path_diag.  Deprecated.  """
        # Should never occur.  Only we should know about 'diag'
        LOGGER.warning('Deprecated: Use "f_path_diag", not "f_deref_diag"')  
        return self.fn_path_diag(arg_list)

    def fn_eval(self, arg_list):
        """ Evaluate the first arg, which is an expression in the form of a Python structure
        Logically:
            eval(json_to_evaluate) 
        """
        return  self.evaluate(self.evaluate(yaml.safe_load(self.evaluate(arg_list[0]))))

    def fn_failIf(self, arg_list):          # pylint: disable=C0103
        """ Fail if any element of arg1 (an array) satisfies arg2, a predicate. Arg3 is optional diagnostic.
        Logically:
            failIf(array, predicate [, diag]) 
        """
        if len(arg_list) == 2:
            return self.fail_if(self.evaluate(arg_list[0]), arg_list[1])
        else:
            return self.fail_if(self.evaluate(arg_list[0]), arg_list[1],
                                self.evaluate(arg_list[2]))

    def fn_findAll(self, arg_list):          # pylint: disable=C0103
        """ Find all elements of arg1 (an array) that satisfy arg2, a predicate.
        Logically:
            findAll(array, predicate) 
        """
        return self.find_all(self.evaluate(arg_list[0]), arg_list[1])

    def fn_findOne(self, arg_list):          # pylint: disable=C0103
        """ Same as fn_findAll, only return as soon as matching element is found.
        Logically:
            findOne(array, predicate, error_message) 
        """
        if len(arg_list) == 2:
            return self.find_one(self.evaluate(arg_list[0]), arg_list[1])
        else:
            return self.find_one(self.evaluate(arg_list[0]), arg_list[1],
                                 False,
                                 self.evaluate(arg_list[2]))        

    def fn_getReadyResource(self, arg_list):          # pylint: disable=C0103
        """ Get resource if ready, else raise EvalPending."""
        return self.get_ready_resource(self.evaluate(arg_list[0]))

##    def fn_path(self, arg_list):
##        """ Evaluate a path expression against either 1st arg, else current document.  
##        Logically:
##            f_path([<context>, ] <path_expr>) 
##        """
##        if len(arg_list) == 1:
##            return self.path_eval_of_bp(self.evaluate(arg_list[0]))
###            return self.path_eval_of_context(self.evaluate(arg_list[0]))
##        else:
##            return self.path_eval(self.evaluate(arg_list[0]), self.evaluate(arg_list[1]))

    def fn_path(self, arg_list):
        """ Evaluate a path expression against the blueprint context.  
        Logically:
            f_path(<path_expr>) 
        """
        return self.path_eval_of_bp(self.evaluate(arg_list[0]))

    def fn_path_diag(self, arg_list):
        """ Version of fn_path for internal use only.  Adds a diagnostic param.  
        Logically:
            f_path_diag(<path_expr>, <diagnostic message>) 
        """
        return self.path_eval_of_bp(self.evaluate(arg_list[0]), self.evaluate(arg_list[1]))

    def fn_pathc(self, arg_list):
        """ Evaluate a path expression against either 1st arg, else current document.  
        Logically:
            f_path([<context>, ] <path_expr>) 
        """
        if len(arg_list) == 1:
            return self.path_eval_of_context(self.evaluate(arg_list[0]))
        else:
            return self.path_eval(self.evaluate(arg_list[0]), self.evaluate(arg_list[1]))

    def fn_pathc_diag(self, arg_list):
        """ Version of fn_path for internal use only.  Adds a diagnostic param.  
        Logically:
            f_path_diag([<context>, ] <path_expr>, <diagnostic message>) 
        """
        if len(arg_list) == 2:
            return self.path_eval_of_context(self.evaluate(arg_list[0]), self.evaluate(arg_list[1]))
        else:
            return self.path_eval(self.evaluate(arg_list[0]), self.evaluate(arg_list[1]), self.evaluate(arg_list[2]))

    def fn_print(self, arg_list):
        """ Print value of 1st arg and optional 2nd message arg.
        Logically:
            print(value[, label_to_print]) 
        """
        value = self.evaluate(arg_list[0])
        if len(arg_list) == 2:
            print '>>> Print-point {1}: \n    Value = {0}'.format(value, self.evaluate(arg_list[1]))
        else:
            print '>>> Print-point: \n    Value = {0}'.format(value)
        return value

    def fn_replace(self, arg_list):          
        """ Return string with first occurrence of 'old' replaced by 'new'.
        Logically:
            replace(str, old, new)
        """
        return string.replace(arg_list[0], arg_list[1], arg_list[2])

    def fn_replacePathExprs(self, arg_list):          # pylint: disable=C0103
        """ Return value of 1st arg, text, but with each !!pathExpr!! replaced by its value."""
        # Notes: The last path expression need not be terminated if it's at the end of the string
        segments = coerce_to_string(self.evaluate(arg_list[0])).split('!!')
        seg_num = 0
        while seg_num + 1 < len(segments):
            segments[seg_num + 1] = self.path_eval_of_context(segments[seg_num + 1])
##            try:
##                segments[seg_num + 1] = self.path_eval_of_context(segments[seg_num + 1])
##            except BlueprintEvalError as exc:
##                raise_eval_error("fn_replacePathExprs: eval error: {0}".format(exc)) # todoh: show context
##                #todo: or should we just leave the erroneous string?
            seg_num += 2
        return ''.join(segments) 
            

    # TODO: Implement optional wait duration param
    def fn_waitForOne(self, arg_list):          # pylint: disable=C0103
        """ 
        Same as fn_findOne, only if none found, throw 'pending' so as to wait and retry.
        Logically:
            waitForOne(listExpression, predicate, [errorMsg[, waitMsg]]) 
        """
        if len(arg_list) == 2:
            return self.find_one(self.evaluate(arg_list[0]), arg_list[1], wait_flag = True)
        elif len(arg_list) == 3:
            return self.find_one(self.evaluate(arg_list[0]), arg_list[1],
                                 True,
                                 self.evaluate(arg_list[2]))        
        else:
            return self.find_one(self.evaluate(arg_list[0]), arg_list[1],
                                 True,
                                 self.evaluate(arg_list[2]), self.evaluate(arg_list[3]))        

    def fn_EQ(self, arg_list):          # pylint: disable=C0103
        """ Return true if 1st and 2nd args are equal.
        Logically:
            EQ(value1, value2) 
        """
        return self.evaluate(arg_list[0]) == self.evaluate(arg_list[1])

    def fn_IN(self, arg_list):          # pylint: disable=C0103
        """ Return true if 1st arg equals any subsequent args in list.  """
        val = self.evaluate(arg_list[0])
        for i in range(1, len(arg_list)):
            if val == self.evaluate(arg_list[i]):
                return True
        return False

    def fn_NE(self, arg_list):          # pylint: disable=C0103
        """ Return true if 1st and 2nd args are not equal.
        Logically:
            NE(value1, value2) 
        """
        return self.evaluate(arg_list[0]) != self.evaluate(arg_list[1])

    def fn_NOT(self, arg_list):          # pylint: disable=C0103
        """ Return true if 1st arg is false.  """
        return not self.evaluate(arg_list[0])

# ==============================================================================
# Section: Interacting with user (aka "debugger")

    def print_expr_result(self, cmd_type, cmd_string):
        """ Print evaluation results for path expr or blueprint function evaluation."""
        eval_string = cmd_string.strip()
        if len(eval_string)== 0:
            return

        result = None
        try:
            if  cmd_type == 'p':
                result = self.path_eval(self.bp_parsed, eval_string)
            else:
                if len(eval_string) > 0:
                    result = self.evaluate(yaml.safe_load(eval_string))
        except BlueprintError as exc:
            show_error(exc)
##        except BlueprintEvalError as exc:
##            show_eval_error(exc)
##        except BlueprintExecError as exc:
##            show_exec_error(exc)
        except BlueprintEvalPending as exc:
            show_eval_pending(exc)
        except yaml.YAMLError as exc:
            print "Error:"
            print exc
            
        if result != None:
            if type(result) in LEGAL_SCALARS:
                print result
            else:
                try:
                    print yaml.safe_dump(result, default_flow_style=False)
                except yaml.representer.RepresenterError:
                    print yaml.dump(result)


    def get_command(self, prompt):
        """ Get a (possibly multi-line) command, categorize it, and return its type and string."""
        line = raw_input(prompt)
        token_plus_remainder = line.split(None, 1)
        
        cmd_type = 'p'
        if len(token_plus_remainder) > 0:
            cmd_type = token_plus_remainder[0]
        cmd_string = ''
        if len(token_plus_remainder) > 1:
            cmd_string = token_plus_remainder[1]
        if cmd_type.lower() in ['p', 'path']:
            return 'p', cmd_string
        elif cmd_type.lower() in ['e', 'eval', 'evaluate']:
            if cmd_string.strip() != '':
                LOGGER.warning('Trailing characters "{0}" will be ignored')
                cmd_string = ''
            str_list = []
            while True:
                next_line = raw_input('  Eval: ')
                if len(next_line) > 0:
                    str_list.append('\n' + next_line)
                else:
                    break
                cmd_string = ''.join(str_list)
##            while True:
##                next_line = raw_input('  Eval: ')
##                if len(next_line) > 0:
##                    cmd_string = cmd_string + '\n' + next_line  # TODO: Use append?
##                else:
##                    break
            return 'e', cmd_string 
        elif cmd_type.lower() in ['h', '?', 'help']:
            return 'h', cmd_string
        elif cmd_type.lower() in ['x', 'xit', 'exit']:
            return 'x', cmd_string
        elif cmd_type.lower() in ['c', 'continue']:
            return 'c', cmd_string
        else:
            return 'p', line


    def prompt_and_eval(self):
        """ Our 'debugger'.  Prompt and evaluate in either Path or Eval mode."""
        print 'For command info, enter (h)elp'
        while True:
            cmd_type, cmd_string = self.get_command('\nPaused: ')
            if cmd_type == 'p' or cmd_type == 'e':
                self.print_expr_result(cmd_type, cmd_string)
            elif cmd_type == 'h':
                print 'Commands are...'
                print '   p[ath] <path expression>: evaluate path expr'
                print '   e[val]:                   read & evaluate blueprint expression'
                print '   c[ontinue]:               continue blueprint instantiation'
                print '   x[it]:                    exit blueprint processor'
                print '   h[elp]:                   (this command)'
                print 'If first token isn\'t a command, the line is treated as a path expression'
            elif cmd_type == 'x':
                response = raw_input('Exit? [y]: ')
                if response.lower() in ['', 'y', 'yes']:
                    sys.exit(0)
            elif cmd_type == 'c':
                return


    def start_pause_timer(self):
        """ Start timer to track time spent in an interactive mode
        """
        self.timing['pause_started'] = time.time()

    def stop_pause_timer(self):
        """ Stop timer to track time spent in an interactive mode
        """
        self.timing['pause_secs'] += time.time() - self.timing['pause_started']


    def enter_debugger(self, phase_text):
        """ Pause prior to a processing phase, keeping track of pause time
        """
        print '\n...Pause point, prior to {0}...'.format(phase_text)
        self.start_pause_timer()
        try:
            self.prompt_and_eval()
        finally:
            self.stop_pause_timer()
        print '...continuing...'


    def get_remaining_time(self, timeout):
        """ Return # of seconds remaining: timeout - processingTime
            (where processingTime is elapsedSecs - pauseSecs)
        """
        elapsed_secs = time.time() - self.timing['start_time']
##        elapsed_secs = time.time() - self.bp_parsed['_instantiation']['start_time']
        return (timeout -
                (elapsed_secs - self.timing['pause_secs']))

# ==============================================================================
# Section: Initialization, e.g. init_*_section, init_oracle_macros, validate, and initialize*

    def init_cloud_section(self):
        """Initialize cloud section of blueprint.  Set it to SimValue or GET it."""
        if self.is_in_sim_mode():
            self.bp_parsed['Cloud'] = SimValue()
            self.bp_parsed['Cloud'].append('Cloud')
        else:
            LOGGER.info('Connecting to cloud: {0} '.format(self.cloud_connection.cloud_uri))
            try:
                self.bp_parsed['Cloud'] = self.cloud_connection.get_resource(
                                                  absolute_uri('/em/cloud', self.cloud_connection.cloud_uri))
##                                             ,     # TODO: remove next line when Jack's API is used
##                                                  'application/oracle.com.cloud.common.Cloud+json')
                if self.bp_parsed['Cloud']['resource_state']['state'] != _CLOUD_READY:
                    msg = 'Cloud not READY.  State={0}; {1}'
                    raise_exec_error(msg.format(self.bp_parsed['Cloud']['resource_state']['state'],
                                                self.bp_parsed['Cloud']['resource_state']['messages'][0]['hint']))
            except httplib2.ServerNotFoundError:
                raise_exec_error('Server not found')
            except BlueprintEvalError:
                raise_exec_error('Error in metadata format expected from cloud.  Is the URL correct?')

    def init_info_section(self, instance_name):
        """Initialize info section of blueprint."""
        self.bp_parsed['Info'] = {}
        self.bp_parsed['Info']['instance_name'] = instance_name
        year, month, day, hour, minute, second, discard, discard, discard = time.localtime()
        #todoL: this release is not localized, but might want to do better here:   
        self.bp_parsed['Info']['uuid'] = uuid.uuid4().hex
        self.bp_parsed['Info']['date'] = '{0}/{1}/{2}'.format(month, day, year)
        self.bp_parsed['Info']['time'] = '{0}:{1}:{2}'.format(hour, minute, second)
        self.bp_parsed['Info']['date_suffix'] = '{0}_{1}_{2}'.format(month, day, year)
        self.bp_parsed['Info']['time_suffix'] = '{0}_{1}_{2}'.format(hour, minute, second)

    def init_oracle_macros(self):
        """ Add (incorporate) Oracle-provided macros to dispatch table, but don't replace user-provided ones.
        Args:
            none, other than self
        Returns:
            (nothing)
        Raises:
            BlueprintExecError: If macro file parsing fails
        Side-effects:
            May print error messages
        """
        macros_text = '{}'
        file_name = APP_DIRECTORY + os.sep + 'oracle_provided_macros.yml'
        try:
            fil = open(file_name, 'r')
            macros_text = fil.read()
        except IOError as exc:
            LOGGER.error('Oracle-provided macros not found:\n' +
                         '{0}\nProceeding without them.'.format(exc))
        
        try:
            macros_parsed = yaml.safe_load(macros_text)
        except yaml.YAMLError as exc:
            raise_exec_error('Error loading Oracle-provided macros ({0}): \n{1}'.format(file_name, exc))
        
        if not isinstance(macros_parsed, dict):
            LOGGER.error('Oracle-provided macro file is ill-formed (not a dict):\n' +
                         '{0}\nProceeding without them.'.format(type(macros_parsed)))
        else:
            for key, val in macros_parsed.items():
                # Add oracle-provided macro to Macros section, unless user-provided 
                # of same name already present.
                if self.bp_parsed['Macros'].get(key) is None:
                    self.bp_parsed['Macros'][key] = val

        # Fold all macros into dispatch table
        macros = self.bp_parsed['Macros']
        for mac_name in iter(macros):
            if mac_name[0:2] != 'f_':
                msg = 'Macro names must begin with "f_".  Macro {0} will be ignored.' 
                LOGGER.error(msg.format(mac_name))
            self.DISPATCH_TABLE[mac_name[2:]] = [macros[mac_name][0], # Min arg count
                                                 macros[mac_name][0], # Max arg count
                                                 macros[mac_name][1]] # Definition
                                                 

    def validate_input_params(self):
        """ Perform LINT-like checking of Input param definitions. Warn of any issues.
        Args:
            none, other than self
        Returns:
            (nothing)
        Raises:
            (nothing)
        Side-effects:
            May print warnings
        """
        for in_param_name in self.bp_parsed['Inputs'].keys():
            in_param = self.bp_parsed['Inputs'][in_param_name]
            if not isinstance(in_param, dict):
                msg = 'Input parameter "{0}" is not well-formed: ({1}).  Will be ignored.'
                LOGGER.warning(msg.format(in_param_name, type(in_param)))
                del self.bp_parsed['Inputs'][in_param_name]
            else:
                if 'Type' in in_param and in_param['Type'] not in self.ALLOWED_INPUT_TYPES:
                    message = ('Input parameter "{0}" specifies type of "{1}".  Will be ignored.' +
                               ' Specify one of: {2}')
                    LOGGER.warning(message.format(in_param_name,
                                                  in_param['Type'],
                                                  self.ALLOWED_INPUT_TYPES))
                if 'Sensitive' in in_param and not isinstance(in_param['Sensitive'], bool):
##                if 'Sensitive' in in_param and (not isinstance(in_param['Sensitive'], bool) or  
##                                                in_param['Sensitive'] not in self.ALLOWED_INPUT_SENSITIVITIES):
                    message = ('Input parameter "{0}" specifies sensitivity "{1}".  Will be ignored.' +
                               ' Specify one of: {2}')
                    LOGGER.warning(message.format(in_param_name,
                                                  in_param['Sensitive'],
                                                  self.ALLOWED_INPUT_SENSITIVITIES))
                for attr_name in in_param:
                    if attr_name not in self.ALLOWED_INPUT_ATTRS:
                        message = ('Input parameter "{0}" specifies {1}.  Will be ignored.' +
                                   ' Specify one of: {2}')
                        LOGGER.warning(message.format(in_param_name,
                                                      attr_name,
                                                      self.ALLOWED_INPUT_ATTRS))


    def validate(self):
        """ Perform LINT-like checking. Warn of any issues.
        Args:
            none, other than self
        Returns:
            (nothing)
        Raises:
            (nothing)
        Side-effects:
            May print warnings
            May modify bp_parsed, e.g. to delete an ill-formed Resources entry, so that
                later code doesn't throw exceptions
        """

        for key in self.bp_parsed:
            if key not in (self.LEGAL_SECTIONS + self.ADDED_SECTIONS):
                LOGGER.warning('Blueprint section "{0}" encountered.  Will be ignored.'.format(key))

        if len(self.bp_parsed['Resources']) < 1:
            LOGGER.warning('No Resources specified in blueprint.  Nothing will be created')
        for res_name in self.bp_parsed['Resources'].keys():
            resource = self.bp_parsed['Resources'][res_name]
            if not isinstance(resource, dict):
                msg = 'Resource "{0}" is not well-formed: ({1}).  Will be ignored.'
                LOGGER.warning(msg.format(res_name, type(resource)))
                del self.bp_parsed['Resources'][res_name]
            else:
                for attr_name in resource:
                    if attr_name not in self.ALLOWED_RESOURCE_SECTIONS:
                        msg = ('Resource {0}, section {1} is not recognized.  Will be ignored.' +
                               ' Specify one of: {2}')
                        LOGGER.warning(msg.format(res_name, attr_name, 
                                       self.ALLOWED_RESOURCE_SECTIONS))
                        
        # Check input param definitions 
        self.validate_input_params()


    def initialize_resources(self):
        """Initialize all resources of blueprint, augmenting what the author provided.
        
        Args:
            none, other than self
        Returns:
            (nothing)
        Raises:
            (nothing)
        Side-effects:
            May print warnings
        """
        for res_name in self.bp_parsed['Resources']:
            resource = self.bp_parsed['Resources'][res_name]
            resource['_state'] = _EVAL_NOT_TRIED
            resource['_subState'] = ''
            resource['_logged_state'] = None
            resource['_uri'] = self.INITIAL_URI_VALUE_PREFIX + res_name
            resource['_create_start_time'] = ''
            resource['_create_end_time'] = ''
            if self.is_in_sim_mode():
                if 'Simulation' not in resource:
                    resource['Simulation'] = {}
                    resource['Simulation']['delay'] = self.DEFAULT_SIMULATION_DELAY
                    LOGGER.debug('No simulation delay specified for {0}, assuming 2.'.format(res_name))
                if 'result' not in resource['Simulation']:
                    resource['Simulation']['result'] = 's'
                    LOGGER.debug('No simulation result specified for {0}, assuming "s".'.
                                    format(res_name))
                if resource['Simulation']['result'] not in ('s', 'f'):
                    LOGGER.warning('Invalid simulation result {0} specified for {1}, assuming "s".'.
                                    format(resource['Simulation']['result'], res_name))
                    resource['Simulation']['result'] = 's'
                if type(resource['Simulation']['delay']) in (str, unicode):
                    try:
                        resource['Simulation']['delay'] = int(resource['Simulation']['delay'])
                    except ValueError:
                        LOGGER.warning('Delay specified for resource {0} was {1}, assuming 2'.
                                        format(res_name, resource['Simulation']['delay']))
                        resource['Simulation']['delay'] = self.DEFAULT_SIMULATION_DELAY


    def _initialize(self, bp_source, cloud_uri, user, password, instance_name,
                    validate_certificate):
        """ Initialize the blueprint, but passing in source text, not file name.
            Used only for testing.
        
        Args:
            bp_source: text of the blueprint
            cloud_uri:
            user:
            password:
            instance_name: name of instance to create.  For now, only used by Info section.
            validate_certificate -- True means to validate the server's certificate
        Returns:
            (nothing)
        Raises:
            BlueprintExecError: e.g. if parsing bp_source or macros fails.  Also http issues.
        Side-effects:
            May print warnings
        """
        # Create internal repr. of the blueprint, i.e. prep it for processing.
        try:
            self.bp_parsed = yaml.safe_load(bp_source)
        except yaml.YAMLError as exc:
            raise_exec_error('Error loading blueprint YAML: \n{0}'.format(exc))

        # Assure that missing sections exist (and are empty)
        for section_name in self.LEGAL_SECTIONS:
            if section_name not in self.bp_parsed:
                self.bp_parsed[section_name] = {}

        self.init_oracle_macros()

        # Determine cloud-access info and whether we're in simulation mode
        # Then, init Cloud section of blueprint runtime        
        self.cloud_connection = CloudConnection(cloud_uri, user, password, validate_certificate)
        self.init_cloud_section()
        self.init_info_section(instance_name)

        # Validate the blueprint, but proceed in any event
        self.validate()

        # Set each resource to initial state of 'evaluating' & no URI
        # Also fill in default simulation values
        self.initialize_resources()

##        # Set up local instantiation values
##        self.bp_parsed['_instantiation'] = dict(start_time = time.time())
##        self.bp_parsed['_instantiation']['pause_secs'] = 0
        
##        # Validate the blueprint, but proceed in any event
##        self.validate()
##

    def initialize(self, file_name, cloud_uri, user, password, instance_name, 
                   validate_certificate):
        """ Initialize the blueprint, prior to instantiating (or using eval mode)

        Args:
            file_name - name of file from which to read blueprint 
            cloud_uri
            user
            password
            instance_name  -- name of instance to create
            validate_certificate -- True means to validate the server's certificate
        Returns:
            (nothing)
        Note:
            Warn of attributes not supported?
            Print output values or eval stack error
        Raises:
            BlueprintExecError: If file can't be read.  Also, see _initialize
        """
        try:
            bp_source = open(file_name, 'r').read()
        except IOError as exc:
            raise_exec_error(exc.strerror + ' - ' + exc.filename)            
        self._initialize(bp_source, cloud_uri, user, password, instance_name,
                         validate_certificate)

# ==============================================================================
# Section: Process Inputs section, i.e. input parameters

    def get_input_order_value(self, param_name):
        """Return the value used for sorting inputs (or a high value to sort last)."""
        try:
            return self.bp_parsed['Inputs'][param_name]['Order']
        except KeyError:
            return 'zzzzzzz' 
        

    def prompt_for_one_input(self, in_param_name):
        """ Prompt for value of in_param not provided in input_dicts.
        Args:
            in_param_name -- name of parameter to prompt for
        Returns:
            value to use for that parameter
        Notes:
            - Caller must assure that in_param_name exists
            - The default value for an input parameter can use intrinsic functions,
              e.g. to access values defined in the Data section.
        """
        return_val = None
        in_param = self.bp_parsed['Inputs'][in_param_name]
        while return_val == None:
            if 'Prompt' in in_param:
                prompt = in_param['Prompt']
            else:
                prompt = in_param_name
            prompt = '   ' + prompt
            if 'DefaultValue' in in_param:
                default_value = in_param['DefaultValue']
                prompt = prompt + ' (' + str(default_value) + '): '
            else:
                default_value = ''
                prompt = prompt + ': '
            
            if 'Sensitive' in in_param and in_param['Sensitive']:
                input_val = getpass.getpass(prompt)
            else:
                input_val = raw_input(prompt)
            if input_val == '' and default_value != '':
                return_val = default_value
            else:
                return_val = input_val
            if 'Type' in in_param and in_param['Type'] == 'Number': 
                try:
                    return_val = string_to_num(return_val)
                except ValueError:
                    LOGGER.warning('Parameter value must be numeric')
                    return_val = None  # Try again
        return return_val
        

    def prompt_for_remaining_inputs(self, input_dict):
        """ Prompt for each param not provided in input_dicts, i.e. fill in remaining input values.
        Args:
            input_dict -- values of input parameters provided on command line
        Returns:
            (nothing)
        Raises:
            BlueprintEvalError: If the default value or prompt can't be evaluated
        Notes:
            - The default value for an input parameter can use intrinsic functions,
              e.g. to access values defined in the Data section.
            - We do not allow an input param to depend on another. 
        """
        header_printed = False
        for in_param_name in sorted(self.bp_parsed['Inputs'], key = self.get_input_order_value):
            try:
                in_param = self.evaluate(self.bp_parsed['Inputs'][in_param_name]) # Evaluate default value, prompt, ...
            except BlueprintEvalPending as exc:
                # Catch a foolish reference to resource to be created.
                raise_eval_error('While processing input parameter {0}: {1}'.format(in_param_name, exc))
            if in_param_name not in input_dict:
                if not header_printed:
                    print '\nInput Parameter Value Entry'
                    print   '---------------------------'
                    header_printed = True
                in_param['Value'] = self.prompt_for_one_input(in_param_name)
            else:
                in_param['Value'] = input_dict[in_param_name]
        if header_printed:
            print ' '

        
    def process_inputs(self, input_dict):
        """Prompt for any needed inputs that are not already in input_dict.

        Args:
            input_dict -- values of input parameters provided on command line
        Returns:
            (nothing)
        Description of input processing:
            Each member of the Inputs section defines an input parameter.
            In addition to its name, it can specify:
                Type:         String or Number.  Default is String
                Prompt:       How to prompt the user for the value
                DefaultValue: (if not provided, the value must be provided
                               via input_dict or interactively)
                Sensitive:    True/False.  Default is False
        Note:
            The default value for an input parameter can use intrinsic functions,
            e.g. to access values defined in the Data section.
        Raises:
            BlueprintEvalError: If the default value or prompt can't be evaluated
        """
        self.start_pause_timer()

        # Check for input_dict values whose names don't match input params
        for in_dict_name in input_dict:
            if in_dict_name not in self.bp_parsed['Inputs']:
                message = ('You provided "{0} = {1}", but no Input entry "{0}" ' +
                           'is defined in blueprint')
                LOGGER.warning(message.format(in_dict_name,
                                              input_dict[in_dict_name]))
            else:
                in_param = self.bp_parsed['Inputs'][in_dict_name]
                in_value = input_dict[in_dict_name]
                if 'Type' in in_param and in_param['Type'] == 'Number': 
                    try:
                        discard = string_to_num(in_value)
                    except ValueError:
                        msg = 'Parameter value for {0} must be numeric, not "{1}"'
                        raise_exec_error(msg.format(in_dict_name, in_value))

##        # Check for input params that contain other non-supported sub-sections
##        for in_param_name in self.bp_parsed['Inputs']:
##            in_param = self.bp_parsed['Inputs'][in_param_name]
##            for attr_name in in_param:
##                if attr_name not in self.ALLOWED_INPUT_ATTRS:
##                    message = ('Blueprint specifies "{0}" for Input parameter {1}.  Will be ignored.\n' +
##                               'Only these can be specified: {2}')
##                    LOGGER.warning(message.format(attr_name,
##                                                  in_param_name,
##                                                  self.ALLOWED_INPUT_ATTRS))
##                                                  
        # Get remaining input params                                                  
        self.prompt_for_remaining_inputs(input_dict)
        self.stop_pause_timer()

# ==============================================================================
# Section: Process Resources section


    def instantiate_resource(self, res_name, timeline, remaining_time):
        """Try to create an instance of a blueprint resource or continue the process.

        Args:
            res_name -- the name of the resource (from the blueprint)
            timeline --
            remaining_time -- # of seconds to allow
        Returns:
            State of the resource (same as it's "_state" entry)
        Raises:
            BlueprintExecError: If no Container, http issue, create attempt fails, etc. 
            BlueprintEvalError: If state returned from resource is EXCEPTION, 
                                expression error, ...
            BlueprintEvalPending: If an expression refers to a non-READY resource
        Notes:
            - Always inform the timeline of state changes unless raising an exception,
              in which case the caller will do so.
            - 
        """
        timeline.more_info(res_name)
        resource = self.bp_parsed['Resources'][res_name]
        if (resource['_state'] == _EVAL_NOT_TRIED or
            resource['_state'] == _EVAL_PENDING):
            try:
                container = self.bp_parsed['Resources'][res_name]['Container']
            except KeyError:
                resource['_state'] = _EVAL_FAILED
                raise_exec_error('Resource {0}: No container specified'.format(res_name))

            try:
                self.evaluate(container)  
                self.evaluate(resource)
                resource['_state'] = _EVAL_SUCCEEDED
                timeline.more_info(res_name)
            except BlueprintError as exc:
                resource['_state'] = _EVAL_FAILED
                raise_error(exc, 'Create of resource {0} failed: {1}'.format(res_name, exc.msg))
##                if isinstance(exc, BlueprintEvalError):
##                    print '=== eval fail'
##                    raise_eval_error('Resource: {0}... Evaluation FAILURE:\n{1}'.format(res_name,
##                                                                                        exc.msg))
##                else:
##                    raise_exec_error('Resource: {0}... Execution FAILURE:\n{1}'.format(res_name,
##                                                                                       exc.msg))
            except BlueprintEvalPending as exc:
                resource['_state'] = _EVAL_PENDING
                resource['_subState'] = exc.msg
                reraise_pending(exc)
##                raise

        if resource['_state'] == _EVAL_SUCCEEDED:
            try:
                resource['_uri'], cloud_res_state = self.post_for_create(res_name, remaining_time)
                resource['_state'] = self.cloud_connection.map_state(cloud_res_state, res_name)
                timeline.more_info(res_name)
            except BlueprintError as exc:
                resource['_state'] = _POST_FAILED
                raise_error(exc, 'Create of resource {0} failed: {1}'.format(res_name, exc.msg))
##                if isinstance(exc, BlueprintEvalError):
##                    raise_eval_error('Resource: {0}... Creation FAILURE:\n{1}'.format(res_name,
##                                                                                      exc.msg))
##                else:
##                    raise_exec_error('Resource: {0}... Creation FAILURE:\n{1}'.format(res_name,
##                                                                                      exc.msg))

        if resource['_state'] == _CREATING:
            try:
                cloud_res_state, diag, details = self.get_cloud_res_state(res_name)
            except BlueprintExecError as exc:
                raise_exec_error('Failure polling resource {0}: {1}'.format(res_name, exc))
            if cloud_res_state == _CLOUD_READY:
                resource['_create_end_time'] = time.time()
            resource['_cloud_status'] = cloud_res_state
            resource['_state'] = self.cloud_connection.map_state(cloud_res_state, res_name)
            timeline.more_info(res_name)
            if resource['_state'] == _CREATE_FAILED:
                msg = 'Create of resource {0} failed: {1}'.format(res_name, diag)
                if len(details) > 0:
                    msg = msg + '\n' + details
                raise_exec_error(msg)

        return resource['_state']


    def process_resources(self, timeout, refresh_frequency):
        """Process the Resources section.  Continue until timeout, all are READY,
           or instantiation of any resource fails.

        Args:
            timeout        -- number of seconds to wait before abandoning the
                              attempt to create all resources.  
            refresh_frequency -- how frequently to emit the elipses in timeline
        Returns:
            (nothing)
        Raises:
            BlueprintEvalError
            BlueprintExecError
        """
        resources = self.bp_parsed['Resources']
        timeline = Timeline(refresh_frequency)
        timeline.init(resources)
        while self.get_remaining_time(timeout) > 0:
            found_an_incomplete = False
            for res_key in iter(resources):
                try:
                    self.instantiate_resource(res_key, timeline, self.get_remaining_time(timeout))
                except BlueprintError as exc:
                    timeline.finish()
                    raise_error(exc, exc.msg)
##                except BlueprintEvalError as exc:
##                    timeline.finish()
##                    reraise_eval_error(exc)
##                except BlueprintExecError as exc:
##                    timeline.finish()
##                    raise_exec_error(str(exc))
                except BlueprintEvalPending as exc:
                    timeline.more_info(res_key)
                if resources[res_key]['_state'] != _READY:
                    found_an_incomplete = True

            if not found_an_incomplete:
                break
            time.sleep(self.ITERATION_DELAY)
        timeline.finish()

# ==============================================================================
# Section: Process Outputs section

    def process_outputs(self, eaten_exceptions):
        """Process the Outputs section and display any outputs

        Args:
            eaten_exceptions - list of exceptions raised during blueprint instantiation
                               to which this function may add
        Returns:
            (nothing)
        Raises:
            None.  Errors for each output are caught and diagnosed.  That way,
            we can continue to show remaining outputs and return so that final
            stats can be summarized. 
        """
        outputs = self.bp_parsed['Outputs']
        if len(outputs) == 0:
            return

        LOGGER.info(' ')
        LOGGER.info('Output Processing')
        LOGGER.info('-----------------')
        LOGGER.info(' ')
        LOGGER.info('Output values specified: {0}'.format(len(outputs)))
        for outputs_key in iter(outputs):
            message_prefix = '   Value of '
            if 'Value' not in outputs[outputs_key]:
                message = message_prefix + '{0}: No "Value" specification provided'
                LOGGER.error(message.format(outputs_key))
                eaten_exceptions.append(BlueprintExecError(message.format(outputs_key)))
            else:
                try:
                    outputs[outputs_key]['Value'] = self.evaluate(outputs[outputs_key]['Value'])
                    LOGGER.info(message_prefix + '{0}: {1}'.format(outputs_key,
                                                                   outputs[outputs_key][u'Value']))
                except BlueprintError as exc:
                    message = message_prefix + '{0}: Expression could not be evaluated.  Error is...'
                    LOGGER.error(message.format(outputs_key))
                    show_error(exc)
                    eaten_exceptions.append(exc)
                except BlueprintEvalPending as exc:
                    message = message_prefix + '{0}: Evaluation refers to resource that is not ready: {1}'
                    LOGGER.info(message.format(outputs_key, exc.msg))
                    eaten_exceptions.append(exc)


# ==============================================================================
# Section: High-level function(s)

    def instantiate_bp(self, input_dict, timeout, pausepoints,
                       refresh_frequency = DEFAULT_REFRESH_FREQ): 
        """Create an instance of this blueprint.

        Args:
            input_dict     -- values of input parameters
            timeout        -- number of seconds to wait before abandoning the
                              attempt to create all resources.  When abandoned
                              at least one resource will be in state 'creating'
                              or 'evaluating'
            pausepoints    -- List of places to stop: before any of input/resource/output/terminate
        Returns:
            eaten_exceptions -- list of exceptions we encountered (but didn't abort)
                                Used only by test harness
        Raises:
            (none) 
            Exceptions are caught and diagnosed.  That's because we generally want 
            to continue, e.g. to show Outputs that are available and the Processing
            Summary section.  
        """    
        eaten_exceptions = []
        self.context = [self.bp_parsed]

        if 'pause_input' in pausepoints:
            self.enter_debugger('Input processing')
        try:
            self.process_inputs(input_dict)  # Defaults can contain expressions, so can throw exceptions
        except BlueprintError as exc:
            eaten_exceptions.append(exc)
            show_error(exc)

        if len(eaten_exceptions) == 0:
            if 'pause_resource' in pausepoints:
                self.enter_debugger('processing Resources section')
            try:
                self.process_resources(timeout, refresh_frequency)  
            except BlueprintError as exc:
                eaten_exceptions.append(exc)
                show_error(exc)

        # Results:

        # Time exceeded?
        if self.get_remaining_time(timeout) <= 0:
            exc = BlueprintExecError('Max time exceeded: {0} seconds...'.format(timeout))
            eaten_exceptions.append(exc)
            show_error(exc)

        # Evaluate and print the outputs
        if 'pause_output' in pausepoints:
            self.enter_debugger('Output processing')
        self.process_outputs(eaten_exceptions)  # May add to eaten_exceptions

        if len(eaten_exceptions) > 0 and 'pause_error' in pausepoints:
            self.enter_debugger('terminating due to error')

        # Resource State Summary
        LOGGER.info(' ')
        LOGGER.info('Blueprint Processing Summary')
        LOGGER.info('----------------------------')
        display_resource_state_summary(self.bp_parsed['Resources'])

        if 'pause_terminate' in pausepoints:
            self.enter_debugger('terminating')

        return eaten_exceptions

# ==============================================================================
# Section: Exception-related utility (raise_*, print_*, and show_*)

def raise_pending(message):
    """Execute common actions when raising a blueprint evaluation pending exception."""
    LOGGER.debug('BlueprintEvalPending: ' + message)
    raise BlueprintEvalPending(message)

def raise_eval_error(message):
    """Execute common actions when handling a blueprint evaluation failure."""
    LOGGER.debug('BlueprintEvalError: ' + message)
    raise BlueprintEvalError(message)

def raise_exec_error(message):
    """Execute common actions when handling a blueprint execution failure."""
    LOGGER.debug('BlueprintExecError: ' + message)
    raise BlueprintExecError(message)

def raise_error(exc, message):
    """Raise a new error using message.  Use 'exc' to determine whether to raise
       an eval or exec error.  If an eval error, use exc (so as to retain expr stack)."""
    if isinstance(exc, BlueprintEvalError):
        exc.msg = message
        reraise_eval_error(exc) 
    elif isinstance(exc, BlueprintExecError):
        raise_exec_error(message)
    else:
        LOGGER.critical('Unanticipated exception.  Not BlueprintError.')
        raise_exec_error(message)

def raise_either_eval_diag(default_diag, override_diag):
    """ Utility function. Raises eval error and pushes both default_diag and 
        override_diag onto evaluation 'stack'; else just default_diag."""
    if override_diag != '':
        raise_eval_error('{0}\n{1}'.format(override_diag, default_diag))
    else:
        raise_eval_error(default_diag)

def reraise_pending(exc):
    """Execute common actions when re-raising a blueprint pending exception."""
    LOGGER.debug('Reraise BlueprintEvalPending: ' + exc.msg)
    raise exc

def reraise_eval_error(exc):
    """Execute common actions when re-raising a blueprint evaluation failure."""
    LOGGER.debug('Reraise BlueprintEvalError: ' + exc.msg)
    raise exc

def show_exec_error(exec_exception):
    """ Present details of expression execution error to user.

    Args:
        exec_exception
    Returns:
        (nothing)
    Raises:
        (nothing)
    Note: 
        This function exists to parallel show_eval_error.
    """
    LOGGER.error(exec_exception.msg)


EXPR_LINE_PREFIX = '|   Expr: '
def show_eval_error(eval_exception):
    """ Present details of expression evaluation error to user.
    An evaluation error occurs in the context of an expression, which we want
    to show the user.  This function shows the expression evaluation 'stack'.

    Args:
        eval_exception
    Returns:
        (nothing)
    Raises:
        (nothing)
    """
    # Format multi-line messages with same prefix
    msg_lines = eval_exception.msg.split('\n')
    for line in msg_lines:
        LOGGER.error(line)
    
    stack = eval_exception.expr_stack
    if len(stack) == 1: 
        LOGGER.error('Expression being evaluated at the time:')
        LOGGER.error('   ' + stack.pop())
    else:
        LOGGER.error('Expression evaluation stack follows (with failed expression at bottom) ...')
        LOGGER.error('-----')
        while len(stack) > 0:
            expr = stack.pop()
            LOGGER.error(EXPR_LINE_PREFIX + expr) 
        LOGGER.error('-----')
        
def show_error(exec_exception):
    """ Present details of either execution or evaluation error to user.

    Args:
        error_exception
    Returns:
        (nothing)
    Raises:
        (nothing)
    """
    if isinstance(exec_exception, BlueprintEvalError):
        show_eval_error(exec_exception)
    else:
        show_exec_error(exec_exception)

def show_eval_pending(eval_exception):
    """ Present details of expression execution error to user.

    Args:
        exec_exception
    Returns:
        (nothing)
    Raises:
        (nothing)
    """
    LOGGER.info(eval_exception.msg)

# ==============================================================================
# Section: Data coercion, normalizing, formatting, and re-formatting functions  

def string_to_num(string_input):
    """ Attempt to convert string to int or float.  Else, throw ValueError. """
    try:
        return int(string_input)
    except ValueError:
        return float(string_input)

def coerce_to_array(val):
    """ If val is not an array, return array containing val """
    # (Special-case strings, since they can appear to be lists.)
    if isinstance(val, str) or isinstance(val, unicode) or type(val) != list:
        return [ val ]
    else:
        return val

def coerce_to_scalar(val):
    """ If val is not a scalar or array of 1 scalar, fail.  Else return scalar. """
    if type(val) in LEGAL_SCALARS:
        return val
    elif type(val) == list and len(val) == 1:
        return val[0]
    else:
        raise_eval_error('Expected scalar but found {0}'.format(val))

def coerce_to_string(val):
    """ If val can't be coerced to a scalar string, fail.  Else return string. """
    if type(coerce_to_scalar(val)) not in [str, unicode]:
        raise_eval_error('Expected string but found {0}'.format(val))
    else:
        return coerce_to_scalar(val)

def normalize_for_escape_syntax(text):
    """Perform escape sequences using Python semantics"""
    return text.decode('string_escape')

def displayable_duration(number_of_secs):
    """Convert number_of_secs to a string of the form 'N minutes, M seconds'."""
    mins, secs = divmod(number_of_secs, 60)
    return '{0} minutes, {1} seconds'.format(int(mins), secs)

def expand_mime_abbrev(text):
    """Look up 'text' in the MIME abbreviations table.  Return the value or 'text' if not found."""
    global _MIME_ABBREVS   # pylint: disable=w0603
    # Initialize table if not done yet
    if _MIME_ABBREVS == None: 
        _MIME_ABBREVS = {}
        file_name = APP_DIRECTORY + os.sep + 'oracle_provided_abbrevs.yml'
        try:
            fil = open(file_name, 'r')
            abbrevs_text = fil.read()
        except IOError as exc:
            LOGGER.debug('Oracle-provided abbrev not found:\n' +
                         '{0}\nProceeding without them.'.format(exc))
        try:
            _MIME_ABBREVS = yaml.safe_load(abbrevs_text)
        except yaml.YAMLError as exc:
            raise_exec_error('Error loading Oracle-provided abbreviationss ({0}): \n{1}'.format(file_name, exc))
        if not isinstance(_MIME_ABBREVS, dict):
            LOGGER.error('Oracle-provided abbrevs file is ill-formed (not a dict):\n' +
                         '{0}\nProceeding without them.'.format(type(_MIME_ABBREVS)))
        
    if text in _MIME_ABBREVS:
        return _MIME_ABBREVS[text]
    else:
        return text
    
def abbreviate_mime_type(text):
    """Look up 'text' in the MIME abbreviations table.  Return its abbreviation or 'text' if not found."""
    #todoh: Factor out the init code
    global _MIME_ABBREVS   # pylint: disable=w0603
    # Initialize table if not done yet
    if _MIME_ABBREVS == None: 
        _MIME_ABBREVS = {}
        file_name = APP_DIRECTORY + os.sep + 'oracle_provided_abbrevs.yml'
        try:
            fil = open(file_name, 'r')
            abbrevs_text = fil.read()
        except IOError as exc:
            LOGGER.debug('Oracle-provided abbrev not found:\n' +
                         '{0}\nProceeding without them.'.format(exc))
        try:
            _MIME_ABBREVS = yaml.safe_load(abbrevs_text)
        except yaml.YAMLError as exc:
            raise_exec_error('Error loading Oracle-provided abbreviationss ({0}): \n{1}'.format(file_name, exc))
        if not isinstance(_MIME_ABBREVS, dict):
            LOGGER.error('Oracle-provided abbrevs file is ill-formed (not a dict):\n' +
                         '{0}\nProceeding without them.'.format(type(_MIME_ABBREVS)))
    
    for key in iter(_MIME_ABBREVS):
        if text == _MIME_ABBREVS[key]:
            return key
    return text
    
# TODO: Needs unit testing...
def replace_in_dicts_and_lists(struct, before, after):
    """Traverse the dict/list structure, replacing 'before' values with 'after'."""
    if isinstance(struct, dict):
        for key, val in struct.items():
            if isinstance(val, dict) or isinstance(val, list):
                replace_in_dicts_and_lists(val, before, after)
            else:
                if val == before:
                    struct[key] = after
    elif isinstance(struct, list):
        for idx, val in enumerate(struct):
            if isinstance(val, dict) or isinstance(val, list):
                replace_in_dicts_and_lists(val, before, after)
            else:
                if val == before:
                    struct[idx] = after

def cloud_to_relative_uri(cloud_uri):
    """ Convert URI from its form '/em/cloud/zzz' to '/zzz'. """
    # TODOL: May not be needed if our ER is implemented
    if isinstance(cloud_uri, SimValue):
        return cloud_uri
    if not isinstance(cloud_uri, basestring):  # is str or unicode
        raise_exec_error('Invalid URI: {0}'.format(cloud_uri))
    if cloud_uri[0:9] != '/em/cloud':
        raise_exec_error('Cloud URI must begin with "/em/cloud", not this: "{0}"'.format(cloud_uri))
    return cloud_uri[9:]


def absolute_uri(uri_param, cloud_uri):
    """ Return absolute URI for uri_param (which may be relative or absolute) . 
        If uri_param begins with a 'http', it's already an absolue URI.
        Else, return the cloud_uri ('http.../em/cloud' + 
                                    <uri_param after '/em/cloud' is stripped off the front>)
    """
    if isinstance(uri_param, SimValue):
        return uri_param
    if uri_param[0:4] == 'http':
        return uri_param
    return cloud_uri + cloud_to_relative_uri(uri_param)

def normalized_cloud_url(url):
    """ Return a 'normalized' or correct cloud URL.
        For instance, a commom mistake is to enter https://myhost/em.
        This would be corrected to be: https://myhost/em/cloud.
    """
    if url.endswith("/"):
        url = url[:len(url) - 1]  # Strip it off

    if url.endswith("em/cloud"):
        return url
    if url.endswith("em"):
        return url + "/cloud"
    else:
        return url + "/em/cloud"
    
def is_uri_set(res_uri):
    """ Return true if res_uri reflects that POST succeeded.  
        (By convention, we initialize a resource definition's _uri value to 
        INITIAL_URI_VALUE_PREFIX.  If it has not been changed, return false.)"""
    return not (res_uri.find(Blueprint.INITIAL_URI_VALUE_PREFIX) == 0)

# ==============================================================================
# Section: Path-expression- or parsing-related

def pop_or_diagnose(stack, diagnostic, user_provided_diag=''):
    """Return top element of stack if any.  Else, throw eval exception with diagnostic.
       This utility function is used only for path expressions with inner tokens that 
       must be 'parsed'."""
    if len(stack) == 0:
        raise_either_eval_diag(diagnostic, user_provided_diag)
    return stack.pop()

def tokenize(text):
    """Return array of tokens for 'text'.  A token is a tuple: (type, value). """

    token_pattern = r"""
    (?P<identifier>[a-zA-Z_][a-zA-Z0-9_]*)
    |(?P<integer>[0-9]+)
    |(?P<dot>\.)
    |(?P<comma>\,)
    |(?P<equals>\=)
    |(?P<dollar>\$)
    |(?P<open_bracket>\[)
    |(?P<close_bracket>\])
    |(?P<whitespace>\s+)
    |(?P<arrow> -> )
    |"  (?P<string>  [^"\\]*(\\.[^"\\]*)* ) "  
    |'  (?P<string2>  [^'\\]*(\\.[^'\\]*)* ) '  
    """

    token_re = re.compile(token_pattern, re.VERBOSE)
    
    tokens = []
    pos = 0
    while True:
        match_obj = token_re.match(text, pos)
        if not match_obj: 
            break
        pos = match_obj.end()
        tokname = match_obj.lastgroup
        tokvalue = match_obj.group(tokname)
        if tokname == 'string2':
            tokname = 'string'
        if tokname == 'string':
            tokvalue = normalize_for_escape_syntax(tokvalue)
        if tokname != 'whitespace':
            tokens.append((tokname, tokvalue))
    if pos != len(text):
        raise_eval_error('No valid token found in path expression at character {0}'.format(pos + 1))
    return tokens


def parse_cmd_line():
    """Parse the command line, returning options and the blueprint filename arg.

    Args:
        none
    Returns:
        options: The 'options' returned by OptionParser.parse_args.
        args: An array of exactly 1 arg.  If the user entered a different number
              that is an error and the OptionParser terminates the application with 
              a diagnostic.
    Raises:
        (Nothing, but >1 argument will cause OptionParser to terminate the application.)
    """
    usage = ("Usage: \n  %prog [options] BlueprintFileName\n\nExample:\n" +
             '  ' + os.path.basename(__file__) + 
             ' myfile.yml -i "name:Joe" -i "count:2" -u jabauer -c https://...')
    parser = OptionParser(usage)
    parser.add_option("-c", "--cloud_uri", dest = "cloud_uri",
                      default =  '',
                      help = "Ex: https://myhost:4473/em/cloud")
    parser.add_option("-u", "--user", dest = "user",
                      default =  None,
                      help = "user id")
    parser.add_option("-p", "--password", dest = "password",
                      default = None,
                      help = "password. If not provided, you'll be prompted.")
    parser.add_option("-t", "--timeout", dest = "timeout",
                      type = "int", default = 5400,  # 90 minutes
                      help = "timeout (seconds)")
    parser.add_option("-i", "--input_value", dest = "input_value",
                      action="append",
                      help = '<param name>:<param value> e.g. -i "name:Joe Blogs"')
    parser.add_option("-r", "--refresh_frequency", dest = "refresh_frequency",
                      type = "int", default = DEFAULT_REFRESH_FREQ,
                      help = "# of seconds between dots in timeline. Zero: no dots")
    parser.add_option("-n", "--instance_name", dest = "instance_name",
                      default = "default_instance_name",
                      help = "name of blueprint instance to create")
    parser.add_option("-d", "--debug",
                      action="store_true", dest="debug", default=False,
                      help="more logging, including http traffic")
    parser.add_option("-E", "--pause_error",
                      action="store_true", dest="pause_error", default=False,
                      help="Drop into debugger if error occurs")
    parser.add_option("-I", "--pause_input",
                      action="store_true", dest="pause_input", default=False,
                      help="Pause before processing Input section")
    parser.add_option("-R", "--pause_resource",
                      action="store_true", dest="pause_resource", default=False,
                      help="Pause before processing Resource section")
    parser.add_option("-O", "--pause_output",
                      action="store_true", dest="pause_output", default=False,
                      help="Pause before processing Output section")
    parser.add_option("-T", "--pause_terminate",
                      action="store_true", dest="pause_terminate", default=False,
                      help="Pause before termination, but after output processing or error")
# Not tested, so don't show option help for this release
##    parser.add_option("-v", "--validate_certificate",
##                      action="store_true", dest="validate_certificate", default=False,
##                      help="Validate certificate of cloud server")
    parser.add_option("-v", "--validate_certificate",
                      action="store_true", dest="validate_certificate", default=False,
                      help=optparse.SUPPRESS_HELP)
    parser.add_option("-g", "--graphic_results", dest = "graphic_results",
                      default =  '',
                      help = "Directory for deployed blueprint graphical report")
    parser.add_option("-G", "--graphic_blueprint", dest = "graphic_blueprint",
                      default =  '',
                      help = "Directory for undeployed blueprint graphical report") 
    (options, args) = parser.parse_args()
    if len(args) != 1:
        parser.error("Expected 1 argument, not {0}".format(len(args)))
    return (options, args)


def parse_input_pairs(options):
    """Convert the option string of Inputs to a dict."""
    input_dict = {}
    if options.input_value is not None:
        for input_pair in options.input_value:
            name_value = input_pair.split(':', 1)
            if len(name_value) != 2:
                LOGGER.error('Input value format must be "<name>:<value>", not {0}'.format(input_pair))
                LOGGER.error('    Will ignore and continue.')
            else:
                input_dict[name_value[0]] = name_value[1]
    return input_dict

def determine_pausepoints(options):
    """Return list of pausepoints and a version suitable for display to user

    Args:
        options: The 'options' returned by OptionParser.parse_args
    Returns:
        pausepoints: List of pause point internal ids
        display_string: User-displayable version in string form
    Raises:
        (nothing)
    """
    pausepoints = []
    pausepoints_display = []
    if options.pause_input:
        pausepoints.append('pause_input')
        pausepoints_display.append('Inputs')
    if options.pause_resource:
        pausepoints.append('pause_resource')
        pausepoints_display.append('Resources')
    if options.pause_output:
        pausepoints.append('pause_output')
        pausepoints_display.append('Outputs')
    if options.pause_terminate:
        pausepoints.append('pause_terminate')
        pausepoints_display.append('Termination')
    if options.pause_error:
        pausepoints.append('pause_error')
        pausepoints_display.append('Error/Termination')

    display_string = ', '.join(pausepoints_display)
    if display_string == '':
        display_string = '(none)'
    return (pausepoints, display_string)


# ==============================================================================
# Section: Resource- or Blueprint-cognizant functions (and main)        

def display_timing_summary(client_cpu, processing_elapsed, paused_elapsed, total_elapsed):
    """Display summary of command, prior to processing"""
    LOGGER.info('Timing Summary (seconds):')
    LOGGER.info('   Client-side CPU time: {0}'.format(client_cpu))
    LOGGER.info('   Elapsed time:')  
    LOGGER.info('      Processing time:    {0}'.format(processing_elapsed))
    LOGGER.info('      Paused time:        {0}'.format(paused_elapsed))
    LOGGER.info('      Total elapsed time: {0}'.format(total_elapsed))

def display_resource_state_summary(resources): 
    """If >0 resources, summarize resulting resources."""
    LOGGER.info(' ')
    if len(resources) == 0:
        LOGGER.info('(No resource definitions specified in blueprint')
    else:
        LOGGER.info('Resource State Summary:')
        for resources_key in iter(resources):
            resource = resources[resources_key]
            LOGGER.info('   {0}: {1}'.format(resources_key, resource['_state']))
            LOGGER.info('      URI: {0}'.format(resource['_uri']))
            if '_cloud_status' in resource:
                LOGGER.info('      Cloud resource state: {0}'.format(resource['_cloud_status']))

            LOGGER.info('      Timing info:')
            start_time = resource['_create_start_time']
            if start_time != '':
                start_time = time.strftime('%H:%M:%S', time.localtime(start_time))
            LOGGER.info('        Creation start:     {0}'.format(start_time))
            end_time = resource['_create_end_time']
            if end_time != '':
                end_time = time.strftime('%H:%M:%S', time.localtime(end_time))
            LOGGER.info('        Creation end:       {0}'.format(end_time))
            if end_time != '':
                duration = resource['_create_end_time'] - resource['_create_start_time']
                duration_string = displayable_duration(round(duration, 1))
                LOGGER.info('        Duration:           {0}'.format(duration_string))

    
def display_invocation_summ(options, input_dict, pausepoints_display, filename):
    """Display summary of command, prior to processing"""

    print "\nBlueprint Processor - Invocation Summary"
    print "----------------------------------------"
    print "   Cloud URI:                {0}".format(options.cloud_uri)
    print "   User:                     {0}".format(options.user)
    print "   Blueprint file:           {0}".format(filename)
    print "   Timeout:                  {0}".format(displayable_duration(options.timeout))
    if options.refresh_frequency > 0:
        print "   Refresh frequency:        {0} seconds".format(options.refresh_frequency)
    else:
        print "   Refresh frequency:        None"        
    print "   Inputs:                   {0}".format(input_dict if len(input_dict) > 0 else '')
    print "   Pause points:             {0}".format(pausepoints_display)
    print "   Debug logging:            {0}".format(options.debug)
    print "   Instance name:            {0}".format(options.instance_name)
    print "   Graphical report dir:     {0}".format(options.graphic_results)
    print "   Versions:"
    print "      Blueprint processor:   {0}".format(PROCESSOR_VERSION)
    print "      Cloud protocol:        {0}".format(X_SPEC_VERSION)
    print ' '
        

# ==============================================================================
# Section: Chart generation        

counter = 0
def gen_unique_id():
    """ Generate a unique id: id_NN. (Used to identify a row of a GraphViz node.) """
    global counter            # pylint: disable=W0603
    counter = counter + 1
    return 'id_' + str(counter)
    
def add_arc_from_external(graph, external_node_name, external_node_label, arc_label, dest_node_name, is_contains):
    """ Add a data arc from an 'external resource' node to the destination node.
        (Factors out logic.  Used by add_data_arc.)
        Inputs:
            graph - graph into which to add the arc
            external_node_name - name of 'external resource' node, which may not yet exist,
                                 known to GraphViz  
            external_node_label - label to use for 'external resource' node
            arc_label - label to use on the arc
            dest_node_name - name of node known to GraphViz (including port syntax) 
        Returns: 
            (nothing)
    """
    if is_contains:
         style_val = 'dotted'
         penwidth_val = '5'
    else:
         style_val = 'solid'
         penwidth_val = '1'
    node = pydot.Node(external_node_name, label= external_node_label, shape='rectangle', 
                      style='filled', fillcolor=CLR_EXTERNAL_RES)
    graph.add_node(node)
    graph.add_edge(pydot.Edge(external_node_name, dest_node_name, label=arc_label,
                              style=style_val, penwidth=penwidth_val))

def try_to_add_arc(graph, dest_node_name, expr, is_contains=False):
    """ Add a data (or containment) arc to the dest_node for each reference to another node in expr.
        Label each with the attribute name.
        Inputs:
            graph - graph into which to add the arc
            dest_node_name - name of node known to GraphViz (including port syntax) 
            expr - expression
            is_contains - True if arc is 'containment'
        Returns: 
            abbrev - '' if no arc generated.  Else, a string that abbreviates the expression
    """
    #todoh: ** generalize to support input.  Generalize to support arc to attribute name.
    #          return the abbrev string if successful.
    
    if is_contains:
         style_val = 'dotted'
         penwidth_val = '5'
    else:
         style_val = 'solid'
         penwidth_val = '1'
    abbrev = ''
    
    #todoh: Support nested references (greater than one?)
    if type(expr) == dict and len(expr) == 1:
        if 'f_getResourceAttr' in expr:
            res_name = expr['f_getResourceAttr'][0]
            attr_name = expr['f_getResourceAttr'][1]
            graph.add_edge(pydot.Edge('res_' + res_name, dest_node_name, label=attr_name,
                                      style=style_val, penwidth=penwidth_val))
            abbrev = 'f_getResourceAttr...'
        elif 'f_getResourceURI' in expr:
            res_name = expr['f_getResourceURI'][0]
            attr_name = 'uri'
            graph.add_edge(pydot.Edge('res_' + res_name, dest_node_name, label=attr_name,
                                      style=style_val, penwidth=penwidth_val))
            abbrev = 'f_getResourceURI...'
        elif 'f_path' in expr:
            path_string = expr['f_path'][0]
            if path_string.startswith('Inputs.'):
                in_name = path_string.split('.', 3)[1]
                graph.add_edge(pydot.Edge('in_' + in_name, dest_node_name,
                                          style=style_val, penwidth=penwidth_val)) 
                abbrev = 'f_path...'
        elif 'f_getZoneURI' in expr and isinstance(expr['f_getZoneURI'][0], str):
            get_res_name = expr['f_getZoneURI'][0]
            add_arc_from_external(graph, 'zone_' + get_res_name, get_res_name, 'URI', dest_node_name, is_contains)
            abbrev = 'f_getZoneURI...'
        elif 'f_getAppCompURI' in expr and isinstance(expr['f_getAppCompURI'][0], str):
            get_res_name = expr['f_getAppCompURI'][0] + '\n(of ' + expr['f_getAppCompURI'][1] + ')'
            add_arc_from_external(graph, 'appComp_' + get_res_name, get_res_name, 'URI', dest_node_name, is_contains)
            abbrev = 'f_getAppCompURI...'
        elif 'f_getTemplateURI' in expr and isinstance(expr['f_getTemplateURI'][0], str):  
            #todoh: is this used, given that the access is via Contains?
            get_res_name = expr['f_getTemplateURI'][0]
            add_arc_from_external(graph, 'template_' + get_res_name, get_res_name, 'URI', dest_node_name, is_contains)
            abbrev = 'f_getTemplateURI...'
    return abbrev

def process_nonscalar_row(graph, rows, res_name, key, value, level):
    """ Logic factored out of recursive function rows_for_expr.  Add to 'rows' either:
        - 1 row representing an abbreviation of the value
        - 1 row plus a row for each value
    Inputs:
      graph:    graph into which all is placed
      rows:     current rows to be appended to
      res_name: name of resource
      value:    the value of a dict entry or bullet
      level:    indent level, used for recursion
    Notes:
        If 'key' is 
    """
    guid = gen_unique_id()
    abbrev = try_to_add_arc(graph, 'res_' + res_name + ':' + guid + ':w', value)
    if abbrev != '':  # the arc was generated
        rows.append_row(key, abbrev, level, port=guid)
    else:
        rows.append_row(key, '', level)
        subrows = rows_for_expr(res_name, graph, value, level + 1)
        rows.append_rows(subrows)
    
def rows_for_expr(res_name, graph, expr, level, is_contains=False):
    """ Recursive function to create a rows object, using the properties passed in 'expr'
    Inputs:
      res_name: name of resource
      graph:    graph into which all is placed
      expr:     the value of 'Properties' of the resource (or subset on recursion)
      level:    indent level, used for recursion
      is_contains: True if 'expr' is specified as Container
    Returns a Rows object
    """
    # Called with level plus label (which could be a bullet)
    # Evaluate the expr and
    #  . If scalar, return a 1-row rowset: label plus value
    #  . If expr matches a chartable pattern, chart it and return label plus string representation
    #  . If dict, construct a rowset: 
    #    . If expr is dict, return label row plus nested rowset for expr
    #    . If expr is list, return bullet row plus nested rowset for expr
    #  . If list, construct a rowset: 
    #    . If expr is dict with 1 element, recurse with label of '- <entryName>' and the value
    #    . Else, return label/bullet row plus nested rowset for expr
    
        
    rows = ResourceRows(res_name)  # Create an empty 'rows' object
    if type(expr) == dict:
        for key in iter(expr):
            value = expr[key]

            #todo: recurse and pattern match at each level; combine this with 'contains',
            #      a list element can also contain a function call value, so combine the dict/list cases
            if type(value) != dict and type(value) != list:
                rows.append_row(key, value, level)
            else:
##                print '--- nonscalar_row: {0}, {1} , {2}'.format(res_name, key, value)
##                process_nonscalar_row(graph, rows, res_name, key, value, level)  # Updates 'rows'
                guid = gen_unique_id()
                if is_contains:
                    dest_suffix = ''
                else:
                    dest_suffix = ':' + guid + ':w'
                abbrev = try_to_add_arc(graph, 'res_' + res_name + dest_suffix, value, is_contains)
                if abbrev != '':  # the arc was generated
                    rows.append_row(key, abbrev, level, port=guid)
                else:
                    rows.append_row(key, '', level)
                    subrows = rows_for_expr(res_name, graph, value, level + 1)
                    rows.append_rows(subrows)
    elif type(expr) == list:
        for list_elem in expr:
            if type(list_elem) != dict and type(list_elem) != list:
                rows.append_row('', list_elem, level)
            elif type(list_elem) == dict and len(list_elem) == 1:  # It's a function call
                only_key = list_elem.keys()[0]
##                print '--- nonscalarList_row: {0}, {1} , {2}'.format(res_name, '', list_elem)
##                process_nonscalar_row(graph, rows, res_name, '', list_elem, level)  # Updates 'rows'

                guid = gen_unique_id()
                if is_contains:
                    dest_suffix = ''
                else:
                    dest_suffix = ':' + guid + ':w'
                abbrev = try_to_add_arc(graph, 'res_' + res_name + dest_suffix, list_elem)
                if abbrev != '':  # the arc was generated
                    rows.append_row('', abbrev, level, port=guid)
                else:
                    rows.append_row(BULLET_STRING + only_key, '', level)
                    subrows = rows_for_expr(res_name, graph, list_elem[only_key], level + 1)
                    rows.append_rows(subrows)
            else:
                rows.append_row('', '', level)
                subrows = rows_for_expr(res_name, graph, list_elem, level + 1)
                rows.append_rows(subrows)
    else:
        print '*** CRITICAL ERROR ***'        
    return rows

def get_res_type_display_name(resource):
    """ Get resource's type, in form suitable for display."""
    if 'Type' in resource:
        res_type = abbreviate_mime_type(resource['Type'])
    else:
        res_type = ''
    return res_type

MAX_DISPLAY_TEXT_LEN = 30
def trim_for_display(text):
    """ Trim text if too long for display """
    if len(text) > MAX_DISPLAY_TEXT_LEN:
        return text[0:MAX_DISPLAY_TEXT_LEN] + '...'
    else:
        return text

def generate_chart(bp_parsed, output_file_name, blu=None):
    """ Generate the GraphViz 'chart'.
    Inputs:
        bp_parsed - parsed blueprint 
        output_file_name - filename (that has been validated) to which to write the chart
        blu - entire blueprint object, after deployment  
    Returns: 
        image_map - text for inclusion in html file
    """
    graph = pydot.Dot(graph_type='digraph', dpi=76)  # 96 is too big, 64 a bit too small

    if 'Inputs' in bp_parsed:
        inputs = bp_parsed['Inputs']
        in_subgraph = pydot.Subgraph('', rank='source')
        for name in iter(inputs):
            node = pydot.Node('in_' + name, label=name, shape='invhouse', 
                              style='filled', fillcolor=CLR_INPUT)
            in_subgraph.add_node(node)
        graph.add_subgraph(in_subgraph) 
    if 'Outputs' in bp_parsed:
        outputs = bp_parsed['Outputs']
        out_subgraph = pydot.Subgraph('', rank='sink')
        for name in iter(outputs):
            output = outputs[name]
            if 'Value' in output:  #todoh: factor the following out (see container, ...)
                abbrev = try_to_add_arc(graph, 'out_' + name, output['Value'])
                if abbrev == '':
                    expr_text = trim_for_display(str(output['Value']))
                    out_label = name + '\n(' + expr_text + ')'
                else:
                    out_label = name + '\n(' + abbrev + ')'
            else:
                out_label = name + '\n(no Value specified)'
            node = pydot.Node('out_' + name, label=out_label, shape='house', 
                              style="filled", fillcolor=CLR_OUTPUT)
            graph.add_node(node)            
            out_subgraph.add_node(node)
            
        graph.add_subgraph(out_subgraph) 
    if 'Resources' in bp_parsed:
        resources = bp_parsed['Resources']
        for res_name in iter(resources):
            resource = resources[res_name]
            res_type = get_res_type_display_name(resource)
            res_label = ResourceLabel()
            res_label.init_name_type(res_name, res_type)
            
            # Add container to graph (and connect it)
            if 'Container' in resource:  # Should always be true for legal blueprint 
                #todoh  hh: If scalar URI, add brown node
##                if 'f_getResourceURI' in resource['Container']:
##                    container_name = str(resource['Container']['f_getResourceURI'][0])
##                    graph.add_edge(pydot.Edge('res_' + container_name, 'res_' + res_name, 
##                                              style='dotted', penwidth='5'))
##                if 'f_getTemplateURI' in resource['Container']:
##                    container_name = str(resource['Container']['f_getTemplateURI'][0])  # todo?: remove str()
##                    node = pydot.Node('cont_' + container_name, label=container_name, 
##                                      shape='rectangle', style='filled', fillcolor=CLR_EXTERNAL_RES)
##                    graph.add_node(node)
##                    graph.add_edge(pydot.Edge('cont_' + container_name, 'res_' + res_name, 
##                                              style='dotted', penwidth='5'))
                tempExpr = {'Container': resource['Container']}
                rows = rows_for_expr(res_name, graph, tempExpr, 0, is_contains=True)
                res_label.append_rows(rows)
    
            # Scan properties and add connections to referenced values
            if 'Properties' in resource:
                rows = rows_for_expr(res_name, graph, resource['Properties'], 0)

            # *** Switch to the active resource
            image_map_URL = None
            if not(blu == None):
                resource = blu.bp_parsed['Resources'][res_name]
                if '_state' in resource: 
                    if resource['_state'] == u'READY':
                        image_map_URL = ('#' + res_name)
                        rows.append_row('Resource state', resource['_state'], 0, CLR_READY_STATUS)
                    else:
                        rows.append_row('Resource state', resource['_state'], 0, CLR_NOT_READY_STATUS)
            res_label.append_rows(rows)
            res_label.finish()
            
            if image_map_URL is None:
                node = pydot.Node('res_' + res_name, label=res_label.value, shape="none")
            else:
                node = pydot.Node('res_' + res_name, label=res_label.value, shape="none",
                                  URL=image_map_URL, tooltip='Click for details ({0})'.format(res_name))
            graph.add_node(node)
    
##    graph.write('dump.dot') # FOR DEBUGGING
    graph.write_png(output_file_name)
    return graph.create_cmap()
    
def html_head(title):
    """ Return the <head> section of the generated html.  This includes the CSS. """
    seq = ['<head>\n',
           '<title>' + title + '</title>\n',
           '<style type="text/css">',
           '#prompt{',
           'color:#534741;', 'text-align:right;',
           '}',
           '#value{',
           'color:#333333;', 'text-align:left;',
           '}',
           'body{',
           'background-color:#efefef;',  # Very light background 
           '}',
           'h1{',
           'font-family:Tahoma,sans-serif; font-size:14px;',
           '}',
           'h2{',
           'font-family:Tahoma,sans-serif; font-size:12px;',
           '}',
           'p{',
           'font-family:Tahoma,sans-serif; font-size:11px;',
           '}',
           'table{font-family:Tahoma,sans-serif; font-size:11px;}'
           '.image_carousel img {',
           '  margin-right: 14px;',
           '  display: block;',
#           '  float: left;',
           '  box-shadow: 3px 3px 1px #ccc;',
           '  -webkit-box-shadow: 3px 3px 1px #ccc;',
           '  -moz-box-shadow: 3px 3px 1px #ccc;',
           '}',
           '</style>',
           '</head>\n'
           ]
    return ''.join(seq)

def generate_html(html_file_name, image_file_name, cmap, blu, bp_filename, 
                  total_elapsed, cloud_uri):
    """ Generate the html file that refers to the image file.
    Inputs:
        html_file_name  - fully qualified file name to create with the HTML
        image_file_name - name of file with chart image
        cmap            - 
        blu -             entire blueprint object after deployment (or None)
        bp_filename     - name of blueprint file to include in report
        total_elapsed   - total elapsed time to include in report
        cloud_uri       - URI of cloud to include in report
    Returns:
        (nothing)
    Notes:
        May raise OSException as part of file I/O.
    """
    if blu != None:
        title = 'Deployed Blueprint Graphical Report'
        resource_count = 0
        created_resource_count = 0
        for res_name in iter(sorted(blu.bp_parsed['Resources'])):
            resource_count += 1
            resource = blu.bp_parsed['Resources'][res_name]
            if resource['_state'] == _READY:
                created_resource_count += 1
    else:
        title = 'Undeployed Blueprint Graphical Report'
    fil = open(html_file_name, 'w')
    seq = ['<html>\n',
           html_head(title), 
           '<img src="oracleLogo.png" alt="Oracle Logo"/>\n',
           '<font face="verdana"> <b> Enterprise Manager </b> Cloud Control 12c</font>\n',
           '<body><h1>' + title + '</h1>\n',
           '<table>\n',
           '<tr>\n',
           '<b> <td id="prompt">Blueprint file:</td> </b> <td id="value">{0}</td>\n'.format(bp_filename),
           '</tr>\n'
           ]
    fil.writelines(seq)
    term_time = time.strftime('%x, %X', time.localtime(time.time()))
    if blu != None:
        displayable_uri = cloud_uri
        if cloud_uri == '':
            displayable_uri = 'Not provided - Results are simulated.'
        seq = ['<tr>\n',
               '<b> <td id="prompt">Cloud URI:</td> </b> <td id="value">{0}</td>\n'.format(displayable_uri),
               '</tr>\n'
               '<tr></tr><tr></tr><tr></tr><tr></tr><tr></tr>\n',  # todo: Better way to place small vert gap?
               '<tr>\n',
               '<b> <td id="prompt">Cloud resources created:</td> </b>' + 
               '   <td align="NN">{0} of {1}</td>\n'.format(created_resource_count, resource_count),
               '</tr>\n',
               '<tr>\n',
               '<b> <td id="prompt">Duration:</td> </b> <td id="value">{0}</td>\n'.format(total_elapsed),
               '</tr>\n',
               '<tr>\n',
               '<b> <td id="prompt">Completion time:</td> </b> <td id="value">{0}</td>\n'.format(term_time),
               '</tr>\n'
               ]
        fil.writelines(seq)
    seq = ['<table>\n',
           '<h1>Graphical Representation: Blueprint and Created Resources</h1>\n',
           '<div class="image_carousel">'
           '<img src="{0}" alt="blueprint chart" usemap="#blueprinttmap" />\n'.format(image_file_name), 
           '</div>'
           '<map name="blueprinttmap">\n',
           cmap + '\n',
           '</map>\n', 
           '<h1>Legend</h1>\n',
           '<div class="image_carousel">'
           '<img src="' + LEGEND_FILENAME + '" border="1" alt="Legend"/>\n'.format(image_file_name), 
           '</div>'
           '<br>\n',
           '</body></html>\n'
          ]
    fil.writelines(seq)
    
    if blu != None:
        fil.write('<h1>Created Cloud Resources (and their current state)</h1>\n')
        for res_name in iter(sorted(blu.bp_parsed['Resources'])):
            resource = blu.bp_parsed['Resources'][res_name]
            if resource['_state'] == _READY:
                res_type = get_res_type_display_name(resource)
                fil.write('<a name="{0}">\n'.format(res_name) +
                          '   <h2>{0} (Type: {1})</h2>\n'.format(res_name, res_type) + 
                          '</a>\n') 
                          
                try:
                    runtime_attributes = blu.get_ready_resource(res_name)
                except BlueprintError as exc:
                    runtime_attributes = dict([('Attributes inaccessible', str(exc))]) 
                 
                if isinstance(runtime_attributes, SimValue):
                    runtime_attributes = dict([('uri', '(simulated URI value)'), 
                                               ('sim_attr_1', 'valuea'),
                                               ('simulated attr_2', 'valueb'),
                                               ('simulated_attr_3', 'valuec'),
                                               ('simulated_attr_4', 'valued'),
                                               ('simulated_attr_5', 'valuee'),
                                               ('simulated_attr_6', 'valuef'),
                                               ('simulated_attr_last', 'valueg')
                                              ]) 
                    
                seq = ['<table>\n',
                       '<tr>\n',
                       '<th id="prompt">Attribute Name</th>\n',
                       '<th id="value">Value</th>\n',
                       '</tr>\n',
                       '<title>' + title + '</title>\n'
                      ]
                fil.writelines(seq)
                fmt = '<tr><td id="prompt"><i>{0}</i></td> <td id="value">{1}</td></tr>\n'
                if 'uri' in runtime_attributes:
                    fil.write(fmt.format('uri', runtime_attributes['uri']))
                    del runtime_attributes['uri']
                for attr_name in iter(sorted(runtime_attributes)):
                    fil.write(fmt.format(attr_name, runtime_attributes[attr_name]))
                fil.write('</table>\n')
        if created_resource_count == 0:
            fil.write('(None created.)')
    fil.close()
    
MAX_FILE_SUFFIX_ATTEMPTS = 99
def gen_vis_report(bp_parsed, blu_deployed, options, bp_filename, total_elapsed):
    """ Create 'directory_name' and write graphical report report to it.  If it exists, rename it first.
    Inputs:
        bp_parsed -     The bp_parsed value of overall blueprint.  (Parsed but not deployed blueprint) 
        blu_deployed -  Overall blueprint object, after deployment (or None if not deployed)
        options -       All command line options, from which name of directory 
                        (to write the report) is read  
        bp_filename -   Name of blueprint file (to be written to the report)
        total_elapsed - Total elapsed time (to be written to the report)
                        Only used if deployed blueprint is provided.
    Returns: 
        (nothing)
    Notes: 
        Exits to system with error if I/O, such as rename directory, fails
    """
    LOGGER.info('')
    LOGGER.info('Graphical Report Generation')
    LOGGER.info('-------------------------------')
    LOGGER.info('')
    if pydotx_not_imported:
        LOGGER.error('The pydot package must be installed first.  See "Blueprint Users Manual"')
        return
    if blu_deployed == None:
        directory_name = options.graphic_blueprint 
    else:
        directory_name = options.graphic_results
    new_dir_name = ''
    if os.path.exists(directory_name):
        for i in range(MAX_FILE_SUFFIX_ATTEMPTS):
            if not os.path.exists(directory_name + str(i)):
                new_dir_name = directory_name + str(i)
                break
        if new_dir_name == '':
            msg = 'Directory/file {0} exists as do {0}1 thru {0}{1}'.format(directory_name, 
                                                                            MAX_FILE_SUFFIX_ATTEMPTS)
            msg = msg + '\nGraphical report will not be generated' 
            LOGGER.error(msg)
            sys.exit(1)
        try:
            os.rename(directory_name, new_dir_name)
        except OSError as exc:
            msg = 'Failure trying to rename {0} to {1}: \n{2}'.format(directory_name, 
                                                                    new_dir_name,
                                                                    exc)
            msg = msg + '\nGraphical report will not be generated' 
            LOGGER.error(msg)
            sys.exit(1)
        LOGGER.info('Directory/file {0} exists and was renamed to {1}'.format(directory_name, 
                                                                              new_dir_name))  

    # Create and populate the directory (with canned files)
    try:
        os.mkdir(directory_name)  
        shutil.copy(APP_DIRECTORY + '/' + LOGO_FILENAME, directory_name + '/' + LOGO_FILENAME)
        shutil.copy(APP_DIRECTORY + '/' + LEGEND_FILENAME, directory_name + '/' + LEGEND_FILENAME)
    except OSError as exc:
        msg = 'Failure trying to create/populate {0}: \n{2}'.format(directory_name, 
                                                                    exc)
        msg = msg + '\nGraphical report will not be generated' 
        LOGGER.error(msg)
        sys.exit(1)
    
    image_file_name = directory_name + '/' + IMAGE_FILENAME  # (Slash works on Windows too)
    html_file_name = directory_name + '/' + HTML_FILENAME
    
    image_map = generate_chart(bp_parsed, image_file_name, blu_deployed)
    generate_html(html_file_name, IMAGE_FILENAME, image_map, blu_deployed, bp_filename, total_elapsed, options.cloud_uri)
    LOGGER.info('Graphical report generated: ' + html_file_name)
            
def main():
    """ Process command line, instantiate blueprint, and print summary"""
    start_time = time.time()
    
    (options, args) = parse_cmd_line()
    pausepoints, pausepoints_display = determine_pausepoints(options)
    input_dict = parse_input_pairs(options)
    
    # Fix common mistakes when specifying cloud URL  
    if options.cloud_uri != '':
        options.cloud_uri = normalized_cloud_url(options.cloud_uri)

    # Prompt for password if needed. (Not in sim mode and user id provided.) 
    if options.cloud_uri != '':
        if options.user == None:
            options.user = raw_input('Cloud user id: ')
        if options.password == None:
            options.password = getpass.getpass()
        
    if options.debug:
        LOGGER.setLevel(logging.DEBUG)
        httplib2.debuglevel = 4
    else:
        LOGGER.setLevel(logging.INFO)
        httplib2.debuglevel = 0
    
    # Validate command line options
    if options.graphic_blueprint != '':
        # Only a Graphical report was requested (-G option)')
        options.cloud_uri = ''
    else:
        display_invocation_summ(options, input_dict, pausepoints_display, args[0])
    
    blu = Blueprint()
    try:
        blu.initialize(args[0], options.cloud_uri, options.user, options.password, 
                       options.instance_name, options.validate_certificate)
    except BlueprintError as exc:
        show_error(exc)
        if options.pause_error or options.pause_terminate:
            blu.enter_debugger('error-induced termination')
        return
    bp_parsed_bef_instantiation = copy.deepcopy(blu.bp_parsed)
    
    # Blueprint Visualization
    if options.graphic_blueprint != '':
        gen_vis_report(blu.bp_parsed, None, options, args[0], '')
        sys.exit()

    # Note: instantiate_bp should not throw exceptions but display diagnostics as needed
    try:
        blu.instantiate_bp(input_dict,
                           options.timeout, 
                           pausepoints, options.refresh_frequency)
    except BlueprintError as exc:
        show_error(exc)
        LOGGER.critical('Unanticipated error handling branch.')
        if options.pause_error or options.pause_terminate:
            blu.enter_debugger('error-induced termination')

    elapsed_secs = time.time() - start_time
    try:
        pause_secs = blu.timing['pause_secs']
    except TypeError:
        pause_secs = 0
    except KeyError: 
        pause_secs = 0
    LOGGER.info(' ')
    user_time, system_time, discard, discard, discard = os.times()
    client_cpu  = displayable_duration(round(user_time + system_time, 3))
    processing_elapsed = displayable_duration(round(elapsed_secs - pause_secs, 1))
    paused_elapsed = displayable_duration(round(pause_secs, 1))
    total_elapsed = displayable_duration(round(elapsed_secs, 1))

    display_timing_summary(client_cpu, processing_elapsed, paused_elapsed, total_elapsed)

    # Results Visualization 
    if options.graphic_results != '':
        gen_vis_report(bp_parsed_bef_instantiation, blu, options, args[0],
                               total_elapsed)
if __name__ == '__main__':
    main()

# todo: Testing
#  . Each pausepoint works as intended. (On 7/16, -E didn't work.)

# Example Invocations:
#
# QA site:
# bp_processor.py interactiveTests\data.yml -c https://pd-em-bigip-vip25.us.oracle.com/em/cloud -I -u jabauer -p JABAUER
#
# Jack's site: (as of 4/9: https://adc2100705.us.oracle.com:4473/em/cloud )
# bp_processor.py interactiveTests/completed/vmviatemplate.yml -c https://adc2100705:4473/em/cloud -u sysman -p sysman
#
# Mcclung's:
# https://adc2100371.us.oracle.com:5416/em/cloud
# Central:
# https://pd-em-bigip-vip30.us.oracle.com/em/cloud
#
# Simulation:
# bp_processor.py interactiveTests/vm.yml  -c sim        -t 300
# 
# Use of pylint:
#
# pylint --reports=n --include-ids=y bp_processor.py --max-line-length=120 --disable=W0511
# and...
# pylint --help-msg=_____

# Use of pychecker:
# pychecker --only bp_processor.py

""" Visualization todo's"
 - Add nesting.  (Need to do this before we can return real world attributes.)
 - Use html package (possibly one that i'll need to implement).  Add table data.
 - Review all new todo's.
 - Filter what i GET (JSON or YAML)
 - Handle output that uses unrecognized expr
 - Add values of outputs when run.  (Link to outputs section in html?)
 - Factor out common code (while doing recursion)
 - Fine tune legend
 - House is too wide.  Change to oval and fix legend
 - Detect case where pydot package is installed but GraphViz is not.
 - Easy, while waiting for runs:
    . Documenation
 - Add 'maxreplace' count arg to 'replace'.  Add tests.
"""
