blob: c14ecba4e10a7e593c132f8da338e96ca95e24ab [file]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
require 'cqmf2'
require 'cqpid'
require 'thread'
require 'socket'
require 'monitor'
module Qmf2
Cqmf2.constants.each do |c|
if c.index('AGENT_') == 0 or c.index('CONSOLE_') == 0 or
c.index('SCHEMA_') == 0 or c.index('SEV_') == 0 or c.index('QUERY') == 0
const_set(c, Cqmf2.const_get(c))
end
end
SCHEMA_TYPE_DATA = Cqmf2::SCHEMA_TYPE_DATA()
SCHEMA_TYPE_EVENT = Cqmf2::SCHEMA_TYPE_EVENT()
SCHEMA_DATA_VOID = Cqmf2::SCHEMA_DATA_VOID()
SCHEMA_DATA_BOOL = Cqmf2::SCHEMA_DATA_BOOL()
SCHEMA_DATA_INT = Cqmf2::SCHEMA_DATA_INT()
SCHEMA_DATA_FLOAT = Cqmf2::SCHEMA_DATA_FLOAT()
SCHEMA_DATA_STRING = Cqmf2::SCHEMA_DATA_STRING()
SCHEMA_DATA_MAP = Cqmf2::SCHEMA_DATA_MAP()
SCHEMA_DATA_LIST = Cqmf2::SCHEMA_DATA_LIST()
SCHEMA_DATA_UUID = Cqmf2::SCHEMA_DATA_UUID()
ACCESS_READ_CREATE = Cqmf2::ACCESS_READ_CREATE()
ACCESS_READ_WRITE = Cqmf2::ACCESS_READ_WRITE()
ACCESS_READ_ONLY = Cqmf2::ACCESS_READ_ONLY()
DIR_IN = Cqmf2::DIR_IN()
DIR_OUT = Cqmf2::DIR_OUT()
DIR_IN_OUT = Cqmf2::DIR_IN_OUT()
SEV_EMERG = Cqmf2::SEV_EMERG()
SEV_ALERT = Cqmf2::SEV_ALERT()
SEV_CRIT = Cqmf2::SEV_CRIT()
SEV_ERROR = Cqmf2::SEV_ERROR()
SEV_WARN = Cqmf2::SEV_WARN()
SEV_NOTICE = Cqmf2::SEV_NOTICE()
SEV_INFORM = Cqmf2::SEV_INFORM()
SEV_DEBUG = Cqmf2::SEV_DEBUG()
##==============================================================================
## EXCEPTIONS
##==============================================================================
class QmfAgentException < RuntimeError
attr :data
def initialize(data)
@data = data
end
def to_s
"QmfAgentException: #{@data}"
end
end
##==============================================================================
## AGENT HANDLER
##==============================================================================
class AgentHandler
def initialize(session)
@_session = session
@_running = false
@_thread = nil
end
##
## Call the "start" method to run the handler on a new thread.
##
def start
@_thread = Thread.new do
run
end
end
##
## Request that the running thread complete and exit.
##
def cancel
@_running = false
@_thread.join if @_thread
@_thread = nil
end
##
## Call the "run" method only if you want the handler to run on your own thread.
##
def run
@_running = true
event = Cqmf2::AgentEvent.new
while @_running do
valid = @_session.impl.nextEvent(event, Cqpid::Duration.SECOND)
if valid and @_running
case event.getType
when Cqmf2::AGENT_AUTH_QUERY
yes = authorize_query(Query.new(event.getQuery()), event.getUserId())
if yes == true
@_session.impl.authAccept(event)
else
@_session.impl.authReject(event)
end
when Cqmf2::AGENT_QUERY
context = QueryContext.new(@_session, event)
get_query(context, Query.new(event.getQuery()), event.getUserId())
when Cqmf2::AGENT_METHOD
context = MethodContext.new(@_session, event)
begin
method_call(context, event.getMethodName(), event.getDataAddr(), event.getArguments(), event.getUserId())
rescue Exception => ex
@_session.impl.raiseException(event, "#{ex}")
end
end
end
end
end
##
## The following methods are intended to be overridden in a sub-class. They are
## handlers for events that occur on QMF consoles.
##
#
# This method will only be invoked if the "allow-queries" option is enabled on the
# agent session. When invoked, it provides the query and the authenticated user-id
# of the querying client.
#
# This method must return true if the query is permitted, false otherwise.
#
def authorize_query(query, user_id); end
#
# This method will only be invoked if the "external" option is "True" on the agent
# session. When invoked, the method should begin the process of responding to a data
# query. The authenticated user-id of the requestor is provided for informational
# purposes. The 'context' variable is used to provide the results back to the requestor.
#
# For each matching Data object, call context.response(data). When the query is complete,
# call context.complete(). After completing the query, you should not use 'context' any
# longer.
#
# Note: It is not necessary to process the query synchronously. If desired, this method
# may store the context for asynchronous processing or pass it to another thread for
# processing. There is no restriction on the number of contexts that may be in-flight
# concurrently.
#
def get_query(context, query, user_id); end
#
# This method is invoked when a console calls a QMF method on the agent. Supplied are
# a context for the response, the method name, the data address of the data object being
# called, the input arguments (a dictionary), and the caller's authenticated user-id.
#
# A method call can end one of two ways: Successful completion, in which the output
# arguments (if any) are supplied; and Exceptional completion if there is an error.
#
# Successful Completion:
# For each output argument, assign the value directly to context (context.arg1 = "value")
# Once arguments are assigned, call context._success().
#
# Exceptional Completion:
# Method 1: Call context._exception(data) where 'data' is a string or a Data object.
# Method 2: Raise an exception (raise "Error Text") synchronously in the method body.
#
# Note: Like get_query, method_call may process methods synchronously or asynchronously.
# This method may store the context for later asynchronous processing. There is no
# restriction on the number of contexts that may be in-flight concurrently.
#
# However, "Method 2" for Exceptional Completion can only be done synchronously.
#
def method_call(context, method_name, data_addr, args, user_id); end
end
class QueryContext
def initialize(agent, context)
@agent = agent
@context = context
end
def response(data)
@agent.impl.response(@context, data.impl)
end
def complete
@agent.impl.complete(@context)
end
end
class MethodContext
def initialize(agent, context)
@agent = agent
@context = context
end
def _success
@agent.impl.methodSuccess(@context)
end
def _exception(ex)
if ex.class == Data
@agent.impl.raiseException(@context, ex.impl)
else
@agent.impl.raiseException(@context, ex)
end
end
def method_missing(name_in, *args)
name = name_in.to_s
if name[name.length - 1] == 61
name = name[0..name.length - 2]
@context.impl.addReturnArgument(name, args[0])
else
super.method_missing(name_in, args)
end
end
end
##==============================================================================
## CONSOLE HANDLER
##==============================================================================
class ConsoleHandler
def initialize(session)
@_session = session
@_running = false
@_thread = nil
end
##
## Call the "start" method to run the handler on a new thread.
##
def start
@_thread = Thread.new do
run
end
end
##
## Request that the running thread complete and exit.
##
def cancel
@_running = false
@_thread.join if @_thread
@_thread = nil
end
##
## Call the "run" method only if you want the handler to run on your own thread.
##
def run
@_running = true
event = Cqmf2::ConsoleEvent.new
while @_running do
valid = @_session.impl.nextEvent(event, Cqpid::Duration.SECOND)
if valid and @_running
case event.getType
when Cqmf2::CONSOLE_AGENT_ADD
agent_added(Agent.new(event.getAgent))
when Cqmf2::CONSOLE_AGENT_DEL
reason = :filter
reason = :aged if event.getAgentDelReason == Cqmf2::AGENT_DEL_AGED
agent_deleted(Agent.new(event.getAgent), reason)
when Cqmf2::CONSOLE_AGENT_RESTART
agent_restarted(Agent.new(event.getAgent))
when Cqmf2::CONSOLE_AGENT_SCHEMA_UPDATE
agent_schema_updated(Agent.new(event.getAgent))
when Cqmf2::CONSOLE_EVENT
event_raised(Agent.new(event.getAgent), Data.new(event.getData(0)), event.getTimestamp, event.getSeverity)
end
end
end
end
##
## The following methods are intended to be overridden in a sub-class. They are
## handlers for events that occur on QMF consoles.
##
#
# A new agent, whose attributes match the console's agent filter, has been discovered.
#
def agent_added(agent); end
#
# A known agent has been removed from the agent list. There are two possible reasons
# for agent deletion:
#
# 1) :aged - The agent hasn't been heard from for the maximum age interval and is
# presumed dead.
# 2) :filter - The agent no longer matches the console's agent-filter and has been
# effectively removed from the agent list. Such occurrences are likely
# to be seen immediately after setting the filter to a new value.
#
def agent_deleted(agent, reason); end
#
# An agent-restart was detected. This occurs when the epoch number advertised by the
# agent changes. It indicates that the agent in question was shut-down/crashed and
# restarted.
#
def agent_restarted(agent); end
#
# The agent has registered new schema information which can now be queried, if desired.
#
def agent_schema_updated(agent); end
#
# An agent raised an event. The 'data' argument is a Data object that contains the
# content of the event.
#
def event_raised(agent, data, timestamp, severity); end
end
##==============================================================================
## CONSOLE SESSION
##==============================================================================
class ConsoleSession
attr_reader :impl
## The options string is of the form "{key:value,key:value}". The following keys are supported:
##
## domain:NAME - QMF Domain to join [default: "default"]
## max-agent-age:N - Maximum time, in minutes, that we will tolerate not hearing from
## an agent before deleting it [default: 5]
## listen-on-direct:{True,False} - If True: Listen on legacy direct-exchange address for backward compatibility [default]
## If False: Listen only on the routable direct address
## strict-security:{True,False} - If True: Cooperate with the broker to enforce strict access control to the network
## - If False: Operate more flexibly with regard to use of messaging facilities [default]
##
def initialize(connection, options="")
@impl = Cqmf2::ConsoleSession.new(connection, options)
end
def set_domain(domain) @impl.setDomain(domain) end
def set_agent_filter(filter) @impl.setAgentFilter(filter) end
def open() @impl.open end
def close() @impl.close end
def agents
result = []
count = @impl.getAgentCount
for i in 0...count
result << Agent.new(@impl.getAgent(i))
end
return result
end
def connected_broker_agent
Agent.new(@impl.getConnectedBrokerAgent)
end
end
##==============================================================================
## AGENT SESSION
##==============================================================================
class AgentSession
attr_reader :impl
## The options string is of the form "{key:value,key:value}". The following keys are supported:
##
## interval:N - Heartbeat interval in seconds [default: 60]
## external:{True,False} - Use external data storage (queries and subscriptions are pass-through) [default: False]
## allow-queries:{True,False} - If True: automatically allow all queries [default]
## If False: generate an AUTH_QUERY event to allow per-query authorization
## allow-methods:{True,False} - If True: automatically allow all methods [default]
## If False: generate an AUTH_METHOD event to allow per-method authorization
## max-subscriptions:N - Maximum number of concurrent subscription queries permitted [default: 64]
## min-sub-interval:N - Minimum publish interval (in milliseconds) permitted for a subscription [default: 3000]
## sub-lifetime:N - Lifetime (in seconds with no keepalive) for a subscription [default: 300]
## public-events:{True,False} - If True: QMF events are sent to the topic exchange [default]
## If False: QMF events are only sent to authorized subscribers
## listen-on-direct:{True,False} - If True: Listen on legacy direct-exchange address for backward compatibility [default]
## If False: Listen only on the routable direct address
## strict-security:{True,False} - If True: Cooperate with the broker to enforce strict access control to the network
## - If False: Operate more flexibly with regard to use of messaging facilities [default]
##
def initialize(connection, options="")
@impl = Cqmf2::AgentSession.new(connection, options)
end
def set_domain(val) @impl.setDomain(val) end
def set_vendor(val) @impl.setVendor(val) end
def set_product(val) @impl.setProduct(val) end
def set_instance(val) @impl.setInstance(val) end
def set_attribute(key, val) @impl.setAttribute(key, val) end
def open() @impl.open end
def close() @impl.close end
def register_schema(cls) @impl.registerSchema(cls.impl) end
def add_data(data, name="", persistent=false)
DataAddr.new(@impl.addData(data.impl, name, persistent))
end
def del_data(addr)
@impl.del_data(addr.impl)
end
def raise_event(data, severity=nil)
if !severity
@impl.raiseEvent(data.impl)
else
@impl.raiseEvent(data.impl, severity)
end
end
end
##==============================================================================
## AGENT PROXY
##==============================================================================
class Agent
attr_reader :impl
def initialize(impl)
@impl = impl
end
def name() @impl.getName end
def epoch() @impl.getEpoch end
def vendor() @impl.getVendor end
def product() @impl.getProduct end
def instance() @impl.getInstance end
def attributes() @impl.getAttributes end
def to_s
"#{vendor}:#{product}:#{instance}"
end
def query(q, timeout=30)
if q.class == Query
q_arg = q.impl
else
q_arg = q
end
dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
result = @impl.query(q_arg, dur)
raise QmfAgentException.new(Data.new(result.getData(0))) if result.getType == Cqmf2::CONSOLE_EXCEPTION
raise "Protocol error, expected CONSOLE_QUERY_RESPONSE, got #{result.getType}" if result.getType != Cqmf2::CONSOLE_QUERY_RESPONSE
data_list = []
count = result.getDataCount
for i in 0...count
data_list << Data.new(result.getData(i))
end
return data_list
end
def load_schema_info(timeout=30)
dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
@impl.querySchema(dur)
end
def packages
result = []
count = @impl.getPackageCount
for i in 0...count
result << @impl.getPackage(i)
end
return result
end
def schema_ids(package)
result = []
count = @impl.getSchemaIdCount(package)
for i in 0...count
result << SchemaId.new(@impl.getSchemaId(package, i))
end
return result
end
def schema(sid, timeout=30)
dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
Schema.new(@impl.getSchema(sid.impl, dur))
end
end
##==============================================================================
## QUERY
##==============================================================================
class Query
attr_reader :impl
def initialize(arg1, arg2=nil, arg3=nil)
if arg1.class == Qmf2::DataAddr
@impl = Cqmf2::Query.new(arg1.impl)
end
end
def addr() DataAddr.new(@impl.getDataAddr()) end
def schema_id() SchemaId.new(@impl.getSchemaId()) end
def predicate() @impl.getPredicate() end
def matches?(data)
map = data
map = data.properties if data.class == Data
@impl.matchesPredicate(map)
end
end
##==============================================================================
## DATA
##==============================================================================
class Data
attr_reader :impl
def initialize(arg=nil)
@schema = nil
if arg == nil
@impl = Cqmf2::Data.new
elsif arg.class == Cqmf2::Data
@impl = arg
elsif arg.class == Schema
@impl = Cqmf2::Data.new(arg.impl)
@schema = arg
else
raise "Unsupported initializer for Data"
end
end
def to_s
"#{@impl.getProperties}"
end
def schema_id
if @impl.hasSchema
return SchemaId.new(@impl.getSchemaId)
end
return nil
end
def set_addr(addr)
@impl.setAddr(addr.impl)
end
def addr
if @impl.hasAddr
return DataAddr.new(@impl.getAddr)
end
return nil
end
def agent
return Agent.new(@impl.getAgent)
end
def update(timeout=5)
dur = Cqpid::Duration.new(Cqpid::Duration.SECOND.getMilliseconds * timeout)
agent = @impl.getAgent
query = Cqmf2::Query.new(@impl.getAddr)
result = agent.query(query, dur)
raise "Update query failed" if result.getType != Cqmf2::CONSOLE_QUERY_RESPONSE
raise "Object no longer exists on agent" if result.getDataCount == 0
@impl = Cqmf2::Data.new(result.getData(0))
return nil
end
def properties
return @impl.getProperties
end
def get_attr(name)
@impl.getProperty(name)
end
def set_attr(name, v)
@impl.setProperty(name, v)
end
def [](name)
get_attr(name)
end
def []=(name, value)
set_attr(name, value)
end
def _get_schema
unless @schema
raise "Data object has no schema" unless @impl.hasSchema
@schema = Schema.new(@impl.getAgent.getSchema(@impl.getSchemaId))
end
end
def method_missing(name_in, *args)
#
# Convert the name to a string and determine if it represents an
# attribute assignment (i.e. "attr=")
#
name = name_in.to_s
attr_set = (name[name.length - 1] == 61)
name = name[0..name.length - 2] if attr_set
#
# We'll be needing the schema to determine how to proceed. Get the schema.
# Note that this call may block if the remote agent needs to be queried
# for the schema (i.e. the schema isn't in the local cache).
#
_get_schema
#
# If the name matches a property name, set or return the value of the property.
#
@schema.properties.each do |prop|
if prop.name == name
if attr_set
return set_attr(name, args[0])
else
return get_attr(name)
end
end
end
#
# If we still haven't found a match for the name, check to see if
# it matches a method name. If so, marshall the arguments and invoke
# the method.
#
@schema.methods.each do |method|
if method.name == name
raise "Sets not permitted on methods" if attr_set
result = @impl.getAgent.callMethod(name, _marshall(method, args), @impl.getAddr)
if result.getType == Cqmf2::CONSOLE_EXCEPTION
raise QmfAgentException, result.getData(0)
end
return result.getArguments
end
end
#
# This name means nothing to us, pass it up the line to the parent
# class's handler.
#
super.method_missing(name_in, args)
end
#
# Convert a Ruby array of arguments (positional) into a Value object of type "map".
#
private
def _marshall(schema, args)
count = 0
schema.arguments.each do |arg|
if arg.direction == DIR_IN || arg.direction == DIR_IN_OUT
count += 1
end
end
raise "Wrong number of arguments: expecter #{count}, got #{arge.length}" if count != args.length
map = {}
count = 0
schema.arguments.each do |arg|
if arg.direction == DIR_IN || arg.direction == DIR_IN_OUT
map[arg.name] = args[count]
count += 1
end
end
return map
end
end
##==============================================================================
## DATA ADDRESS
##==============================================================================
class DataAddr
attr_reader :impl
def initialize(arg, agentName="")
if arg.class == Hash
@impl = Cqmf2::DataAddr.new(arg)
elsif arg.class == Cqmf2::DataAddr
@impl = arg
else
@impl = Cqmf2::DataAddr.new(arg, agentName)
end
end
def ==(other)
return @impl == other.impl
end
def as_map() @impl.asMap end
def agent_name() @impl.getAgentName end
def name() @impl.getName end
def agent_epoch() @impl.getAgentEpoch end
end
##==============================================================================
## SCHEMA ID
##==============================================================================
class SchemaId
attr_reader :impl
def initialize(impl)
@impl = impl
end
def type() @impl.getType end
def package_name() @impl.getPackageName end
def class_name() @impl.getName end
def hash() @impl.getHash end
end
##==============================================================================
## SCHEMA
##==============================================================================
class Schema
attr_reader :impl
def initialize(arg, packageName="", className="", kwargs={})
if arg.class == Cqmf2::Schema
@impl = arg
else
@impl = Cqmf2::Schema.new(arg, packageName, className)
@impl.setDesc(kwargs[:desc]) if kwargs.include?(:desc)
@impl.setDefaultSeverity(kwargs[:sev]) if kwargs.include?(:sev)
end
end
def finalize() @impl.finalize end
def schema_id() SchemaId.new(@impl.getSchemaId) end
def desc() @impl.getDesc end
def sev() @impl.getDefaultSeverity end
def add_property(prop) @impl.addProperty(prop.impl) end
def add_method(meth) @impl.addMethod(meth.impl) end
def properties
result = []
count = @impl.getPropertyCount
for i in 0...count
result << SchemaProperty.new(@impl.getProperty(i))
end
return result
end
def methods
result = []
count = @impl.getMethodCount
for i in 0...count
result << SchemaMethod.new(@impl.getMethod(i))
end
return result
end
end
##==============================================================================
## SCHEMA PROPERTY
##==============================================================================
class SchemaProperty
attr_reader :impl
def initialize(arg, dtype=nil, kwargs={})
if arg.class == Cqmf2::SchemaProperty
@impl = arg
else
@impl = Cqmf2::SchemaProperty.new(arg, dtype)
@impl.setAccess(kwargs[:access]) if kwargs.include?(:access)
@impl.setIndex(kwargs[:index]) if kwargs.include?(:index)
@impl.setOptional(kwargs[:optional]) if kwargs.include?(:optional)
@impl.setUnit(kwargs[:unit]) if kwargs.include?(:unit)
@impl.setDesc(kwargs[:desc]) if kwargs.include?(:desc)
@impl.setSubtype(kwargs[:subtype]) if kwargs.include?(:subtype)
@impl.setDirection(kwargs[:direction]) if kwargs.include?(:direction)
end
end
def name() @impl.getName end
def access() @impl.getAccess end
def index?() @impl.isIndex end
def optional?() @impl.isOptional end
def unit() @impl.getUnit end
def desc() @impl.getDesc end
def subtype() @impl.getSubtype end
def direction() @impl.getDirection end
def to_s
name
end
end
##==============================================================================
## SCHEMA METHOD
##==============================================================================
class SchemaMethod
attr_reader :impl
def initialize(arg, kwargs={})
if arg.class == Cqmf2::SchemaMethod
@impl = arg
else
@impl = Cqmf2::SchemaMethod.new(arg)
@impl.setDesc(kwargs[:desc]) if kwargs.include?(:desc)
end
end
def name() @impl.getName end
def desc() @impl.getDesc end
def add_argument(arg) @impl.addArgument(arg.impl) end
def arguments
result = []
count = @impl.getArgumentCount
for i in 0...count
result << SchemaProperty.new(@impl.getArgument(i))
end
return result
end
def to_s
name
end
end
end