def stop_service(service_name, kill=False, message_service=None):
    """
    Stop a specific service.

    :param service_name: The name of the service.
    :type service_name: str
    :param kill: ``True`` to force the service to stop. ``False`` to
        gently inform the service that it should stop.
    :type kill: bool
    :param message_service: An instance of
        :class:`systemlink.messagebus.message_service.MessageService`.
        May be ``None`` in which case a temporary instance will be
        used.
    :type message_service: systemlink.messagebus.message_service.MessageService
        or None
    """
    if message_service:
        own_message_service = False
    else:
        own_message_service = True
        connection_manager = AmqpConnectionManager()
        builder = MessageServiceBuilder('ServiceManagerClient')
        builder.connection_manager = connection_manager
        message_service = MessageService(builder)

    try:
        broadcast = service_manager_messages.SvcMgrStopMultipleServicesBroadcast(
            service_name, '', kill)
        message_service.publish_broadcast(broadcast)
    finally:
        if own_message_service:
            message_service.close()
            message_service = None
            connection_manager.close()
            connection_manager = None
예제 #2
0
def _setup_amqp_connection():
    '''
    Opens the AMQP connection and instantiates a message service
    '''
    try:
        global CONNECTION_MANAGER  # pylint: disable=global-statement
        global MESSAGE_SERVICE  # pylint: disable=global-statement

        if MESSAGE_SERVICE:
            MESSAGE_SERVICE.close()
            MESSAGE_SERVICE = None

        if CONNECTION_MANAGER:
            CONNECTION_MANAGER.close()
            CONNECTION_MANAGER = None

        master_config = AmqpConfigurationManager.get_configuration(
            id_=SKYLINE_MASTER_CONFIGURATION_ID, enable_fallbacks=False)
        service_name = 'SaltCalibrationMonitoring'
        connection_timeout = 50

        CONNECTION_MANAGER = AmqpConnectionManager(config=master_config)
        CONNECTION_MANAGER.connection_timeout = connection_timeout
        CONNECTION_MANAGER.auto_reconnect = False
        message_service_builder = MessageServiceBuilder(service_name)
        message_service_builder.connection_manager = CONNECTION_MANAGER

        MESSAGE_SERVICE = MessageService(message_service_builder)
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            'Unexpected exception in "nisysmgmt_calibration_monitoring._setup_amqp_connection": %s',
            exc,
            exc_info=True)
        _set_disconnected_beacon_interval()
    def _process_message(self, message):  # pylint: disable=too-many-branches
        """
        Used to allow invoking user defined callbacks for incoming messages
        at various levels.

        Only one is invoked per message, in the following priority:
            1) Callbacks with ``state`` specific to the corrlation ID.
            2) Callbacks without ``state`` specific to the message name.
            3) General callbacks without ``state`` (user defined data).

        :param message: The incoming message to process.
        :type message: systemlink.messagebus.generic_message.GenericMessage
        """
        msg_name_callback_info = None
        corr_id_info = None
        if message.correlation_id in self._corr_id_callbacks:
            corr_id_info = self._corr_id_callbacks[message.correlation_id]
            del self._corr_id_callbacks[message.correlation_id]
        if message.message_name in self._msg_name_callbacks:
            msg_name_callback_info = self._msg_name_callbacks[
                message.message_name]
        if corr_id_info is not None:
            corr_id_info.callback(message, corr_id_info._state)  # pylint: disable=protected-access
        elif msg_name_callback_info is not None:
            if self._explicit_ack and not msg_name_callback_info.explicit_ack:
                self.acknowledge_message(message.consumer_tag,
                                         message.deliver_tag)
            if msg_name_callback_info.callback is not None:
                msg_name_callback_info.callback(message)
        elif self._callback is not None:
            self._callback(message)
        elif message.has_error():
            msg = ('Unhandled message ' + message.message_name + ' Origin=' +
                   message.origin + ' Error=' + str(message.error))
            LOGGER.error(msg)
            if self._trace_logger is not None:
                self._trace_logger.log_error(msg, skip_if_has_log_handler=True)
        elif self._trace_logger is not None and self._trace_unhandled_message.is_enabled:
            self._trace_logger.log(self._trace_unhandled_message,
                                   message.message_name)
        else:
            # pylint: disable=fixme
            # TODO add support for creating a unhandled message callback.
            # pylint: enable=fixme
            msg = 'Unhandled message: ' + message.message_name
            if self._amqp_connection_manager is not None:
                self._amqp_connection_manager.put_status_message(
                    msg, False, replace_if_full=True)
            else:
                AmqpConnectionManager.get_instance().put_status_message(
                    msg, False, replace_if_full=True)
        self._purge_correlation_id_callbacks()
 def __init__(self, builder=None):
     """
     :param builder: A
         :class:`systemlink.messagebus.message_publisher_builder.MessagePublisherBuilder`
         object used in the construction of this object. May be ``None`` if default
         behavior is desired.
     :type builder:
         systemlink.messagebus.message_publisher_builder.MessagePublisherBuilder
     """
     if builder is None:
         builder = MessagePublisherBuilder()
     self._connection_manager = builder.connection_manager
     if self._connection_manager is None:
         self._connection_manager = AmqpConnectionManager.get_instance()
     LOGGER.debug('MessagePublisher\'s connection_manager: %s',
                  self._connection_manager)
     self._origin_name = builder.origin
     self._reply_to = builder.reply_to
     self._exchange_name = self._connection_manager.exchange_name
     self._channel = self._connection_manager.create_publisher_channel()
     self._trace_logger = builder.trace_logger
     self._trace_raw_messages = None
     if self._trace_logger:
         self._trace_raw_messages = self._trace_logger.make_trace_point(
             'RawMessages')
    def _setup_amqp_connection(self):
        '''
        Open the AMQP connection and instantiate the message service
        '''
        master_config = AmqpConfigurationManager.get_configuration(
            id_=SKYLINE_MASTER_CONFIGURATION_ID, enable_fallbacks=False)
        service_name = 'AssetPerformanceManagementSaltService'
        connection_timeout = 50

        self._connection_manager = AmqpConnectionManager(config=master_config)
        self._connection_manager.connection_timeout = connection_timeout
        self._connection_manager.auto_reconnect = False
        message_service_builder = MessageServiceBuilder(service_name)
        message_service_builder.connection_manager = self._connection_manager

        self._message_service = MessageService(message_service_builder)
def start_service(service_name, message_service=None):
    """
    Start a specific service.

    :param service_name: The name of the service.
    :type service_name: str
    :param message_service: An instance of
        :class:`systemlink.messagebus.message_service.MessageService`.
        May be ``None`` in which case a temporary instance will be
        used.
    :type message_service: systemlink.messagebus.message_service.MessageService
        or None
    :param silent: If ``False``, will print status to stdout/stderr.
        If ``True``, will not print status.
    :type silent: bool
    """
    if message_service:
        own_message_service = False
    else:
        own_message_service = True
        connection_manager = AmqpConnectionManager()
        builder = MessageServiceBuilder('ServiceManagerClient')
        builder.connection_manager = connection_manager
        message_service = MessageService(builder)

    try:
        node_name = None
        services = get_services(message_service=message_service)
        for service in services:  # pylint: disable=not-an-iterable
            if service.name == service_name:
                node_name = service.node_name
                break

        if node_name is None:
            error_info = 'Service name "{0}" not found.'.format(service_name)
            raise SystemLinkException.from_name('Skyline.Exception',
                                                info=error_info)

        broadcast = service_manager_messages.SvcMgrStartServicesBroadcast(
            False, [node_name], False, [service_name], 1)
        message_service.publish_broadcast(broadcast)
    finally:
        if own_message_service:
            message_service.close()
            message_service = None
            connection_manager.close()
            connection_manager = None
def get_services(message_service=None):
    """
    Get information for all services.

    :param message_service: An instance of
        :class:`systemlink.messagebus.message_service.MessageService`.
        May be ``None`` in which case a temporary instance will be
        used.
    :type message_service: systemlink.messagebus.message_service.MessageService
        or None
    :return: A list of service information.
    :rtype:
        list(systemlink.messagebus.service_manager_messages.ServiceInstanceBase)
    """
    if message_service:
        own_message_service = False
    else:
        own_message_service = True
        connection_manager = AmqpConnectionManager()
        builder = MessageServiceBuilder('ServiceManagerClient')
        builder.connection_manager = connection_manager
        message_service = MessageService(builder)

    try:
        request = service_manager_messages.SvcMgrGetServiceInfoSnapshotRequest(
        )
        generic_message = message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)

        response = service_manager_messages.SvcMgrGetServiceInfoSnapshotResponse.from_message(
            generic_message)
        return response.services
    finally:
        if own_message_service:
            message_service.close()
            message_service = None
            connection_manager.close()
            connection_manager = None
 def __init__(self,  # pylint: disable=too-many-arguments
              message_service=None,
              service_name='TestMonitorClient',
              config=None,
              connection_timeout=5,
              auto_reconnect=True):
     """
     :param message_service: An instance of the message
         service to use or ``None`` to allow this object to create and own
         the message service.
     :type message_service: systemlink.messagebus.message_service.MessageService or None
     :param service_name: If `message_service` is ``None`` and therefore
         this object creates and owns the message service, the name to
         use for this message service. Try to pick a unique name for
         your client.
     :type service_name: str or None
     :param config: If ``message_service`` is ``None``, the configuration to use for this
         message service. If this is ``None``, will use the default configuration.
     :type config: systemlink.messagebus.amqp_configuration.AmqpConfiguration or None
     :param connection_timeout: Timeout, in seconds, to use
         when trying to connect to the message broker.
     :type connection_timeout: float or int
     """
     self._closing = False
     self._own_message_service = False
     self._connection_manager = None
     if message_service:
         self._message_service = message_service
     else:
         self._connection_manager = AmqpConnectionManager(config=config)
         self._connection_manager.connection_timeout = connection_timeout
         self._connection_manager.auto_reconnect = auto_reconnect
         message_service_builder = MessageServiceBuilder(service_name)
         message_service_builder.connection_manager = self._connection_manager
         self._message_service = MessageService(message_service_builder)
         self._own_message_service = True
    def __init__(self, builder):
        """
        :param builder: A :class:`systemlink.messagebus.amqp_consumer_builder.AmqpConsumerBuilder`
            object used in the construction of this object.
        :type builder: systemlink.messagebus.amqp_consumer_builder.AmqpConsumerBuilder
        """
        self._closing = False
        self._trace_logger = None
        self._trace_raw_messages = None
        self._queue_name = builder.queue_name
        self._routing_key_prefix = builder.routing_key_prefix
        self._durable_queue = builder.durable_queue
        self._callback = builder.callback
        self._message_returned_callback = builder.message_returned_callback
        self._event_args_callback = builder.event_args_callback
        self._auto_reconnect = builder.auto_reconnect
        self._message_handling_thread_should_stop = False  # pylint: disable=invalid-name
        self._monitoring_thread_should_stop = False
        self._should_handle_messages = False
        self._is_handling_messages = False
        self._channel = None
        self._monitoring_thread = None
        self._message_handling_thread = None

        self._connection_manager = builder.connection_manager
        if self._connection_manager is None:
            self._connection_manager = AmqpConnectionManager.get_instance()
        self._exchange_name = self._connection_manager.exchange_name
        self.trace_logger = builder.trace_logger
        self._explicit_ack = builder.explicit_ack
        self._channel = self._connection_manager.create_consumer_channel()
        self._connection_manager.queue_declare(self._channel, self._queue_name,
                                               self._durable_queue, False,
                                               not self._durable_queue)
        self._channel.queue_name = self._queue_name
        self._connection_manager.basic_qos(self._channel, 0,
                                           builder.prefetch_queue_depth, False)
        self._consumer_tag = ''
        self._monitoring_thread = None
        if self._auto_reconnect:
            self._monitoring_thread = threading.Thread(
                target=self._monitoring_thread_func)
            self._monitoring_thread.daemon = True
            self._monitoring_thread.start()
        self._message_handling_thread = threading.Thread(
            target=self._message_handling_thread_func)
        self._message_handling_thread.daemon = True
        self._message_handling_thread.start()
class AssetPerformanceManagmentAmqpWriter(object):  # pylint: disable=too-few-public-methods
    '''
    Abstraction over AMQP connection which facilitates communication
    between the minion and the AssetPerformanceManagement service.
    '''
    __instance = None

    @staticmethod
    def has_systemlink_sdk():
        '''
        Check if the module sucessfully loaded NI Skyline Message Bus
        '''
        return HAS_SYSTEMLINK_SDK

    def __new__(cls):
        '''
        Handle instantiation so that only a single instance of this class is created
        '''
        if not AssetPerformanceManagmentAmqpWriter.has_systemlink_sdk():
            raise AssetPerformanceManagmentAmqpWriterException(
                'Import of NI Skyline Message Bus failed.')

        if AssetPerformanceManagmentAmqpWriter.__instance is None:
            AssetPerformanceManagmentAmqpWriter.__instance = object.__new__(
                cls)
            # pylint: disable=protected-access
            AssetPerformanceManagmentAmqpWriter.__instance._initialized = False
            AssetPerformanceManagmentAmqpWriter.__instance._connection_manager = None
            AssetPerformanceManagmentAmqpWriter.__instance._message_service = None
            # pylint: enable=protected-access
        return AssetPerformanceManagmentAmqpWriter.__instance

    def __init__(self):
        '''
        Initialize the message service if not already initialized
        '''
        if not self._initialized:
            try:
                self._initialize_message_service()
            except SystemLinkException as exc:
                self._close_message_service()
                if exc.error.name == 'Skyline.AMQPErrorFailedToLogIn':
                    # The salt-master may have changed credentials after the salt-minion
                    # connects to the salt-master through Salt.
                    raise AssetPerformanceManagmentAmqpWriterException(
                        'AMQP Authentication error. Credentials may have changed',
                        exc,
                        is_warning=True)
                else:
                    # All other AMQP exceptions
                    raise AssetPerformanceManagmentAmqpWriterException(
                        'An AMQP error has occurred', exc)

    def _initialize_message_service(self):
        '''
        Initialize the message service

        :return: ``True`` if initialized successfully, ``False`` otherwise.
        :rtype: bool
        '''
        file_path = paths.get_skyline_master_file()
        if not os.path.isfile(file_path):
            # The Skyline Master file is not available.
            # Can't set up the Message Service without it.
            return

        self._setup_amqp_connection()

        self._initialized = True

    def _setup_amqp_connection(self):
        '''
        Open the AMQP connection and instantiate the message service
        '''
        master_config = AmqpConfigurationManager.get_configuration(
            id_=SKYLINE_MASTER_CONFIGURATION_ID, enable_fallbacks=False)
        service_name = 'AssetPerformanceManagementSaltService'
        connection_timeout = 50

        self._connection_manager = AmqpConnectionManager(config=master_config)
        self._connection_manager.connection_timeout = connection_timeout
        self._connection_manager.auto_reconnect = False
        message_service_builder = MessageServiceBuilder(service_name)
        message_service_builder.connection_manager = self._connection_manager

        self._message_service = MessageService(message_service_builder)

    def _close_message_service(self):
        '''
        Close the AMQP connection and the message service
        '''
        if self._message_service:
            self._message_service.close()
            self._message_service = None
        if self._connection_manager:
            self._connection_manager.close()
            self._connection_manager = None

        self._initialized = False

    @classmethod
    def cleanup(cls):
        '''
        Clean up the writer by closing the connection
        '''
        # This makes sure that if the salt-minion closes before the writer has been initialized
        # we don't create it just to clean it up.
        if cls.__instance is not None:
            cls.__instance._close_message_service()  # pylint: disable=protected-access
            cls.__instance = None

    def publish_minion_assets_updated_broadcast(self, broadcast):  # pylint: disable=invalid-name
        '''
        Publish an AssetPerformanceManagementMinionAssetsUpdatedBroadcast over AMQP
        '''
        try:
            self._message_service.publish_broadcast(broadcast)
        except SystemLinkException as exc:
            raise AssetPerformanceManagmentAmqpWriterException(
                'An AMQP error has occurred', exc)
class TestMonitorClient():
    """
    Class to publicly access the Test Monitor Client.
    """
    def __init__(self,  # pylint: disable=too-many-arguments
                 message_service=None,
                 service_name='TestMonitorClient',
                 config=None,
                 connection_timeout=5,
                 auto_reconnect=True):
        """
        :param message_service: An instance of the message
            service to use or ``None`` to allow this object to create and own
            the message service.
        :type message_service: systemlink.messagebus.message_service.MessageService or None
        :param service_name: If `message_service` is ``None`` and therefore
            this object creates and owns the message service, the name to
            use for this message service. Try to pick a unique name for
            your client.
        :type service_name: str or None
        :param config: If ``message_service`` is ``None``, the configuration to use for this
            message service. If this is ``None``, will use the default configuration.
        :type config: systemlink.messagebus.amqp_configuration.AmqpConfiguration or None
        :param connection_timeout: Timeout, in seconds, to use
            when trying to connect to the message broker.
        :type connection_timeout: float or int
        """
        self._closing = False
        self._own_message_service = False
        self._connection_manager = None
        if message_service:
            self._message_service = message_service
        else:
            self._connection_manager = AmqpConnectionManager(config=config)
            self._connection_manager.connection_timeout = connection_timeout
            self._connection_manager.auto_reconnect = auto_reconnect
            message_service_builder = MessageServiceBuilder(service_name)
            message_service_builder.connection_manager = self._connection_manager
            self._message_service = MessageService(message_service_builder)
            self._own_message_service = True

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def __del__(self):
        self.close()

    def close(self):
        """
        Close the TestMonitorClient and all associated resources.
        """
        if self._closing:
            return
        self._closing = True
        if self._own_message_service:
            self._message_service.close()
            self._connection_manager.close()

    def create_results(self, results):
        """
        Create one or more test results

        :param results: A list of dicts. Each dict must have the following keys:
            status (:class:`systemlink.testmonclient.messages.Status` or `str`):
                    Test status.
            startedAt (:class:`datetime`):
                    Test start time.
            programName (:class:`str`):
                    Test program name.
            systemId (:class:`str`):
                    Identifier for the system that ran this test.
            hostName (:class:`str`):
                    Host machine name.
            operator (:class:`str`):
                    Operator name for this result.
            serialNumber (:class:`str`):
                    Serial number for the Unit Under Test.
            totalTimeInSeconds (:class:`float` or :class:`int`):
                    Test duration time in seconds.
            keywords (``list(str)``):
                    A list of keywords associated with the result.
            properties (``dict(str)``):
                    Key/value pairs of properties associated with the result.
            fileIds (``list(str)``):
                    List of fileIds associated with the result.
        :type results: list(dict)
        """
        result_create_requests = []
        for test in results:
            test_status = test['status']
            if isinstance(test_status, str):
                test_status = testmon_messages.Status(
                    testmon_messages.StatusType.from_string(test_status.upper()), test_status)
                test['status'] = test_status.to_dict()
            result_create_request = testmon_messages.ResultCreateRequest.from_dict(test)
            result_create_requests.append(result_create_request)

        request = testmon_messages.TestMonitorCreateTestResultsRequest(result_create_requests)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorCreateTestResultsResponse.from_message(generic_message)
        LOGGER.debug('message = %s', res)

        return res

    def update_results(self, updates, replace=False, determine_status_from_steps=False):
        """
        Update one or more test results

        :param updates: A list of dicts. Each dict must have the following keys:
            status (:class:`systemlink.testmonclient.messages.Status` or `str`):
                    Test status.
            startedAt (:class:`datetime`):
                    Test start time.
            programName (:class:`str`):
                    Test program name.
            systemId (:class:`str`):
                    Identifier for the system that ran this test.
            operator (:class:`str`):
                    Operator name for this result.
            serialNumber (:class:`str`):
                    Serial number for the Unit Under Test.
            totalTimeInSeconds (:class:`float` or :class:`int`):
                    Test duration time in seconds.
            keywords (``list(str)``):
                    A list of keywords associated with the result.
            properties (``dict(str)``):
                    Key/value pairs of properties associated with the result.
            fileIds (``list(str)``):
                    List of fileIds associated with the result.
        :type updates: list(dict)
        :param replace: Indicates if keywords, properties, and file ids should replace the existing
            collection, or be merged with the existing collection.
        :type replace: bool
        :param determine_status_from_steps: Indicates if the status should be set based on the
            status of the related steps.
        :type determine_status_from_steps: bool
        """
        result_update_requests = []
        for update in updates:
            test_status = update['status']
            if isinstance(test_status, str):
                test_status = testmon_messages.Status(
                    testmon_messages.StatusType.from_string(test_status.upper()),
                    test_status)
                update['status'] = test_status.to_dict()
            result_update_request = testmon_messages.ResultUpdateRequest.from_dict(update)
            result_update_requests.append(result_update_request)

        request = testmon_messages.TestMonitorUpdateTestResultsRequest(
            result_update_requests,
            replace,
            determine_status_from_steps)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorUpdateTestResultsResponse.from_message(generic_message)
        LOGGER.debug('message = %s', res)

        return res

    def delete_results(self, ids, delete_steps=True):
        """
        Delete one or more test results

        :param ids: A list of the IDs of the results to delete.
        :type ids: list(str)
        :param delete_steps: Whether or not to delete the results corresponding steps.
        :type delete_steps: bool
        """
        request = testmon_messages.TestMonitorDeleteResultsRequest(ids, delete_steps)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorDeleteResultsResponse.from_message(generic_message)
        LOGGER.debug('message = %s', res)

        return res

    def delete_all_results(self):
        """
        Delete all results
        """
        routed_message = testmon_messages.TestMonitorDeleteAllResultsRoutedMessage()
        self._message_service.publish_routed_message(routed_message)

    def query_results(self, query=None, skip=0, take=-1):
        """
        Return results that match query

        :param query: Object indicating query parameters.
        :type query: systemlink.testmonclient.messages.ResultQuery
        :param skip: Number of results to skip before searching.
        :type skip: int
        :param take: Maximum number of results to return.
        :type take: int
        :return: Results that matched the query.
        :rtype: tuple(list(systemlink.testmonclient.messages.ResultResponse), int)
        """
        request = testmon_messages.TestMonitorQueryResultsRequest(query, skip, take)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorQueryResultsResponse.from_message(generic_message)
        LOGGER.debug('TotalCount: %d', res.total_count)

        return res.results, res.total_count

    def create_steps(self, steps):
        """
        Create one or more step results

        :param steps: A list of dicts. Each dict must have the following keys:
            name (:class:`str`):
                    Step name.
            stepType (:class:`str`):
                    Step type.
            stepId (:class:`str`):
                    Step Identifier.
            parentId (:class:`str`):
                    Identifier for the step parent.
            resultId (:class:`str`):
                    Identifier for the result that this step is associated with.
            status (:class:`systemlink.testmonclient.messages.Status` or `str`):
                    Step status.
            totalTimeInSeconds (:class:`float` or :class:`int`):
                    Step duration time in seconds.
            startedAt (:class:`datetime`):
                    Step start time.
            dataModel (:class:`str`):
                    The name of the data structure in the stepData list. This value identifies
                    the key names that exist in the stepData parameters object. It is generally
                    used to provide context for custom UIs to know what values are expected.
            stepData (``list(stepData)``):
                    A list of data objects for the step.  Each element contains a text string and
                    a parameters dictionary of string:string key-value pairs.
            children (``list(str)``):
                    A list of step ids that define other steps in the request that are children of
                    this step.  The ids in this list must exist as objects in the in the request.
        """
        step_create_requests = []
        for step in steps:
            step_status = step['status']
            if isinstance(step_status, str):
                step_status = testmon_messages.Status(
                    testmon_messages.StatusType.from_string(step_status.upper()),
                    step_status)
                step['status'] = step_status.to_dict()
            step_create_request = testmon_messages.StepCreateRequest.from_dict(step)
            step_create_requests.append(step_create_request)

        request = testmon_messages.TestMonitorCreateTestStepsRequest(step_create_requests)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorCreateTestStepsResponse.from_message(generic_message)
        LOGGER.debug('message = %s', res)

        return res

    def update_steps(self, steps):
        """
        Update one or more steps

        :param updates: A list of dicts. Each dict must at least have step_id and result_id. The
            other dict members are used to update the step, if present:
            name (:class:`str`):
                    Step name.
            stepType (:class:`str`):
                    Step type
            stepId (:class:`str`):
                    Step Identifier.
            parentId (:class:`str`):
                    Identifier for the step parent.
            resultId (:class:`str`):
                    Identifier for the result that this step is associated with.
            status (:class:`systemlink.testmonclient.messages.Status` or `str`):
                    Step status.
            totalTimeInSeconds (:class:`float` or :class:`int`):
                    Step duration time in seconds.
            startedAt (:class:`datetime`):
                    Step start time.
            dataModel (:class:`str`):
                    The name of the data structure in the stepData list.  This value identifies
                    the key names that exist in the stepData parameters object.  It is generally
                    used to provide context for custom UIs to know what values are expected.
            stepData (``list(stepData)``):
                    A list of data objects for the step.  Each element contains a text string and
                    a parameters dictionary of string:string key-value pairs.
            children (``list(str)``):
                    A list of step ids that define other steps in the request that are children of
                    this step.  The ids in this list must exist as objects in the in the request.
        """
        step_update_requests = []
        for step in steps:
            if 'status' in step:
                step_status = step['status']
                if isinstance(step_status, str):
                    step_status = testmon_messages.Status(
                        testmon_messages.StatusType.from_string(step_status.upper()),
                        step_status)
                    step['status'] = step_status.to_dict()
            step_update_request = testmon_messages.StepUpdateRequest.from_dict(step)
            step_update_requests.append(step_update_request)
        request = testmon_messages.TestMonitorUpdateTestStepsRequest(step_update_requests)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorUpdateTestStepsResponse.from_message(generic_message)
        LOGGER.debug('message = %s', res)

        return res

    def delete_steps(self, steps):
        """
        Delete one or more steps

        :param steps: A list of dicts. Each dict must have the following keys:
            stepId (:class:`str`):
                    Step Identifier.
            resultId (:class:`str`):
                    Identifier for the result that this step is associated with.
        :type steps: list(dict)
        """
        delete_steps = []

        for step in steps:
            delete_step = testmon_messages.StepDeleteRequest.from_dict(step)
            delete_steps.append(delete_step)

        request = testmon_messages.TestMonitorDeleteStepsRequest(delete_steps)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorDeleteStepsResponse.from_message(generic_message)
        LOGGER.debug('message = %s', res)

        return res

    def query_steps(self, query=None, skip=0, take=-1):
        """
        Return steps that match query

        :param query: Object indicating query parameters.
        :type query: systemlink.testmonclient.messages.StepQuery
        :param skip: Number of steps to skip before searching.
        :type skip: int
        :param take: Maximum number of steps to return.
        :type take: int
        :return: Results that matched the query.
        :rtype: tuple(list(systemlink.testmonclient.messages.StepResponse), int)
        """

        request = testmon_messages.TestMonitorQueryStepsRequest(query, skip, take)
        generic_message = self._message_service.publish_synchronous_message(request)
        if generic_message is None:
            raise SystemLinkException.from_name('Skyline.RequestTimedOut')
        if generic_message.has_error():
            raise SystemLinkException(error=generic_message.error)
        LOGGER.debug('generic_message = %s', generic_message)
        res = testmon_messages.TestMonitorQueryStepsResponse.from_message(generic_message)
        LOGGER.debug('TotalCount: %d', res.total_count)

        return res.steps, res.total_count