cirrocumulus 0.6.3 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +1 -22
- data/lib/cirrocumulus.rb +8 -2
- data/lib/cirrocumulus/{message.rb → agents/message.rb} +0 -0
- data/lib/cirrocumulus/channels.rb +129 -0
- data/lib/cirrocumulus/channels/jabber.rb +168 -0
- data/lib/cirrocumulus/environment.rb +46 -0
- data/lib/cirrocumulus/facts.rb +135 -0
- data/lib/cirrocumulus/identifier.rb +84 -0
- data/lib/cirrocumulus/ontology.rb +333 -49
- data/lib/cirrocumulus/pattern_matching.rb +175 -0
- data/lib/cirrocumulus/remote_console.rb +29 -0
- data/lib/cirrocumulus/rule_queue.rb +69 -0
- data/lib/cirrocumulus/rules/engine.rb +7 -5
- data/lib/cirrocumulus/rules/fact.rb +22 -0
- data/lib/cirrocumulus/rules/pattern_matcher.rb +19 -0
- data/lib/cirrocumulus/rules/run_queue.rb +1 -0
- data/lib/cirrocumulus/saga.rb +44 -25
- data/lib/console.rb +96 -0
- metadata +79 -72
- data/.document +0 -5
- data/.idea/.name +0 -1
- data/.idea/.rakeTasks +0 -7
- data/.idea/cirrocumulus.iml +0 -87
- data/.idea/encodings.xml +0 -5
- data/.idea/misc.xml +0 -5
- data/.idea/modules.xml +0 -9
- data/.idea/scopes/scope_settings.xml +0 -5
- data/.idea/vcs.xml +0 -7
- data/.idea/workspace.xml +0 -614
- data/Gemfile +0 -18
- data/Gemfile.lock +0 -43
- data/Rakefile +0 -37
- data/VERSION +0 -1
- data/cirrocumulus.gemspec +0 -106
- data/lib/.gitignore +0 -5
- data/lib/cirrocumulus/agent_wrapper.rb +0 -67
- data/lib/cirrocumulus/jabber_bus.rb +0 -140
- data/lib/cirrocumulus/rule_engine.rb +0 -2
- data/lib/cirrocumulus/rule_server.rb +0 -49
- data/test/Gemfile +0 -3
- data/test/Gemfile.lock +0 -27
- data/test/helper.rb +0 -18
- data/test/test.rb +0 -85
- data/test/test2.rb +0 -30
- data/test/test_cirrocumulus.rb +0 -7
data/README.rdoc
CHANGED
@@ -1,24 +1,3 @@
|
|
1
1
|
= Cirrocumulus
|
2
2
|
|
3
|
-
Cirrocumulus is an agent-based
|
4
|
-
|
5
|
-
== Technical
|
6
|
-
=== Message structure
|
7
|
-
|
8
|
-
Each message is serialized in XML.
|
9
|
-
It contains routing information, action type and content. Content always is an s-expression.
|
10
|
-
|
11
|
-
Message structure looks like:
|
12
|
-
<fipa-message
|
13
|
-
receiver="optional receiving agent identifier"
|
14
|
-
ontology="domain, e.g. cirrocumulus-vps, cirrocumulus-xen"
|
15
|
-
act="request|query-ref|query-if|inform|failure">
|
16
|
-
<content>
|
17
|
-
(s-expression)
|
18
|
-
</content>
|
19
|
-
</fipa-message>
|
20
|
-
|
21
|
-
=== Action types
|
22
|
-
* request - asks receiver to perform some action, described in content
|
23
|
-
* query-ref - query receiver for some information
|
24
|
-
* query-if - asks receiver, if given proposition in content is true or false
|
3
|
+
Cirrocumulus is an agent-based infrastructure management system. Each agent, running on a host, is responsible for its own problem and cooperates with the rest via XMPP as a transport and FIPA-ACL messages as a protocol.
|
data/lib/cirrocumulus.rb
CHANGED
@@ -1,2 +1,8 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
1
|
+
require 'thread'
|
2
|
+
require 'sexpistol'
|
3
|
+
require_relative 'cirrocumulus/identifier'
|
4
|
+
require_relative 'cirrocumulus/facts'
|
5
|
+
require_relative 'cirrocumulus/channels/jabber'
|
6
|
+
require_relative 'cirrocumulus/channels'
|
7
|
+
require_relative 'cirrocumulus/ontology'
|
8
|
+
require_relative 'cirrocumulus/environment'
|
File without changes
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require_relative 'channels/jabber'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Communication channel between agents. This is an abstract class.
|
5
|
+
#
|
6
|
+
class AbstractChannel
|
7
|
+
#
|
8
|
+
# Informs the other side that given proposition is true.
|
9
|
+
#
|
10
|
+
def inform(sender, proposition, options = {}); end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Request the other side to perform an action
|
14
|
+
#
|
15
|
+
def request(sender, action, options = {}); end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Query the other side about expression value.
|
19
|
+
#
|
20
|
+
def query(sender, expression, options = {}); end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Query the other side if given proposition is true.
|
24
|
+
#
|
25
|
+
def query_if(sender, proposition, options = {}); end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Communication channel between different threads on local machine.
|
30
|
+
#
|
31
|
+
class ThreadChannel < AbstractChannel
|
32
|
+
def initialize(ontology)
|
33
|
+
@ontology = ontology
|
34
|
+
end
|
35
|
+
|
36
|
+
def inform(sender, proposition, options = {})
|
37
|
+
@ontology.handle_inform(sender, proposition, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def request(sender, action, options = {})
|
41
|
+
@ontology.handle_request(sender, action, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def query(sender, expression, options = {})
|
45
|
+
@ontology.handle_query(sender, expression, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
def query_if(sender, proposition, options = {})
|
49
|
+
@ontology.handle_query_if(sender, proposition, options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Communication channel over network.
|
55
|
+
#
|
56
|
+
class NetworkChannel < AbstractChannel
|
57
|
+
def initialize(client, remote_jid)
|
58
|
+
@remote_jid = remote_jid
|
59
|
+
@client = client
|
60
|
+
@serializer = Sexpistol.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def inform(sender, proposition, options = {})
|
64
|
+
client.queue(serializer.to_sexp(build_message(remote_jid, :inform, proposition, options)))
|
65
|
+
end
|
66
|
+
|
67
|
+
def request(sender, action, options = {})
|
68
|
+
client.queue(serializer.to_sexp(build_message(remote_jid, :request, action, options)))
|
69
|
+
end
|
70
|
+
|
71
|
+
def query(sender, expression, options = {})
|
72
|
+
client.queue(serializer.to_sexp(build_message(remote_jid, :query, expression, options)))
|
73
|
+
end
|
74
|
+
|
75
|
+
def query_if(sender, proposition, options = {})
|
76
|
+
client.queue(serializer.to_sexp(build_message(remote_jid, :query_if, proposition, options)))
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
attr_reader :client
|
82
|
+
attr_reader :remote_jid
|
83
|
+
attr_reader :serializer
|
84
|
+
|
85
|
+
def build_message(receiver_name, act, content, options)
|
86
|
+
msg = [
|
87
|
+
act,
|
88
|
+
[:receiver,
|
89
|
+
[:agent_identifier, :name, receiver_name]
|
90
|
+
],
|
91
|
+
[:content, content]
|
92
|
+
]
|
93
|
+
|
94
|
+
msg << [:reply_with, options[:reply_with]] if options.has_key?(:reply_with)
|
95
|
+
msg << [:in_reply_to, options[:in_reply_to]] if options.has_key?(:in_reply_to)
|
96
|
+
msg << [:conversation_id, options[:conversation_id]] if options.has_key?(:conversation_id)
|
97
|
+
|
98
|
+
msg
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Factory to retrieve proper communication channel for two agents.
|
105
|
+
#
|
106
|
+
class ChannelFactory
|
107
|
+
def self.retrieve(instance, agent)
|
108
|
+
if agent.is_a?(LocalIdentifier)
|
109
|
+
ontology_instance = Ontology.query_ontology_instance(agent)
|
110
|
+
|
111
|
+
if ontology_instance
|
112
|
+
return ThreadChannel.new(Ontology.query_ontology_instance(agent))
|
113
|
+
else
|
114
|
+
puts "[WARN] Thread-local ontology not found for identifier=%s" % agent.to_s
|
115
|
+
end
|
116
|
+
elsif agent.is_a?(RemoteIdentifier)
|
117
|
+
jabber_client = JabberChannel.query_client(instance.to_s)
|
118
|
+
|
119
|
+
if jabber_client
|
120
|
+
return NetworkChannel.new(jabber_client, agent.to_s)
|
121
|
+
else
|
122
|
+
puts "[WARN] No active Jabber clients."
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
puts "[WARN] No suitable channel found for #{agent.to_s} (#{agent.class.name})"
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'xmpp4r'
|
2
|
+
require 'xmpp4r-simple'
|
3
|
+
require 'guid'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
class JabberIdentifier < RemoteIdentifier
|
7
|
+
def initialize(jid)
|
8
|
+
@jid = "#{Cirrocumulus::Environment.current.name}-#{jid}"
|
9
|
+
@channel = JabberChannel.new('172.16.11.4', 'cirrocumulus')
|
10
|
+
@channel.connect(@jid, 'q1w2e3r4')
|
11
|
+
@thrd = Thread.new do
|
12
|
+
parser = Sexpistol.new
|
13
|
+
while true do
|
14
|
+
@channel.tick()
|
15
|
+
|
16
|
+
instance = Ontology.query_ontology_instance(self)
|
17
|
+
if !instance.nil?
|
18
|
+
while true
|
19
|
+
msg = @channel.received_messages.pop(true) rescue nil
|
20
|
+
break if msg.nil?
|
21
|
+
|
22
|
+
begin
|
23
|
+
fipa_message = parser.parse_string(msg.body)
|
24
|
+
id = RemoteIdentifier.new(msg.from.resource)
|
25
|
+
|
26
|
+
next if fipa_message.size < 1
|
27
|
+
|
28
|
+
fipa_message = fipa_message.first
|
29
|
+
|
30
|
+
next if fipa_message.size < 2
|
31
|
+
|
32
|
+
act = fipa_message[0]
|
33
|
+
fipa_message.delete_at(0)
|
34
|
+
content = fipa_message
|
35
|
+
|
36
|
+
receiver = nil
|
37
|
+
action_content = nil
|
38
|
+
options = {}
|
39
|
+
content.each do |parameter|
|
40
|
+
next if !parameter.is_a?(Array) || parameter.size < 1
|
41
|
+
if parameter[0] == :receiver
|
42
|
+
receiver = parameter[1][2]
|
43
|
+
elsif parameter[0] == :content
|
44
|
+
action_content = parameter[1]
|
45
|
+
elsif [:reply_with, :in_reply_to, :conversation_id].include?(parameter[0])
|
46
|
+
options[parameter[0]] = parameter[1]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
next if receiver.nil? || receiver != @jid
|
51
|
+
|
52
|
+
case act
|
53
|
+
when :query
|
54
|
+
instance.handle_query(id, action_content, options)
|
55
|
+
when :query_if
|
56
|
+
instance.handle_query_if(id, action_content, options)
|
57
|
+
when :inform
|
58
|
+
instance.handle_inform(id, action_content, options)
|
59
|
+
when :request
|
60
|
+
instance.handle_request(id, action_content, options)
|
61
|
+
end
|
62
|
+
rescue Exception => ex
|
63
|
+
puts ex.message
|
64
|
+
puts ex.backtrace.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
sleep 0.1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def join
|
74
|
+
@thrd.join
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
@jid
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class JabberChannel
|
83
|
+
class << self
|
84
|
+
@@jabber_clients = []
|
85
|
+
|
86
|
+
def query_client(jid)
|
87
|
+
@@jabber_clients.find {|c| c.jid == jid}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :jid
|
92
|
+
attr_reader :conference
|
93
|
+
|
94
|
+
def initialize(server, conference)
|
95
|
+
@jabber = nil
|
96
|
+
@server = server
|
97
|
+
@conference = conference
|
98
|
+
@send_q = Queue.new
|
99
|
+
@recv_q = Queue.new
|
100
|
+
|
101
|
+
@@jabber_clients << self
|
102
|
+
end
|
103
|
+
|
104
|
+
def connected?
|
105
|
+
@jabber && @jabber.connected?
|
106
|
+
end
|
107
|
+
|
108
|
+
def connect(jid, password)
|
109
|
+
@full_jid = "%s@%s" % [jid, @server]
|
110
|
+
@jid = jid
|
111
|
+
|
112
|
+
puts "Using jid #{@jid}"
|
113
|
+
|
114
|
+
begin
|
115
|
+
@jabber = Jabber::Simple.new(@full_jid, password)
|
116
|
+
rescue Jabber::ClientAuthenticationFailure => ex
|
117
|
+
puts ex.class.name
|
118
|
+
client = Jabber::Client.new(@full_jid)
|
119
|
+
client.connect()
|
120
|
+
client.register(password)
|
121
|
+
client.close()
|
122
|
+
@jabber = Jabber::Simple.new(@full_jid, password)
|
123
|
+
rescue Exception => ex
|
124
|
+
puts ex.to_s
|
125
|
+
end
|
126
|
+
|
127
|
+
join_conference(@conference) if connected?
|
128
|
+
connected?
|
129
|
+
end
|
130
|
+
|
131
|
+
def disconnect()
|
132
|
+
@jabber.disconnect()
|
133
|
+
end
|
134
|
+
|
135
|
+
def queue(msg)
|
136
|
+
@send_q << msg
|
137
|
+
end
|
138
|
+
|
139
|
+
def received_messages
|
140
|
+
@recv_q
|
141
|
+
end
|
142
|
+
|
143
|
+
def tick()
|
144
|
+
return if !connected?
|
145
|
+
|
146
|
+
@jabber.received_messages do |msg|
|
147
|
+
next unless msg.x('jabber:x:delay').nil?
|
148
|
+
|
149
|
+
@recv_q << msg
|
150
|
+
end
|
151
|
+
|
152
|
+
while true do
|
153
|
+
to_send = @send_q.pop(true) rescue nil
|
154
|
+
break if to_send.nil?
|
155
|
+
|
156
|
+
@jabber.send!('<message type="groupchat" to="%s" id="%s"><body>%s</body></message>' % [
|
157
|
+
"%s@conference.%s" % [@conference, @server], Guid.new.to_s.gsub('-', ''),
|
158
|
+
to_send.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
159
|
+
])
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
def join_conference(conference)
|
166
|
+
@jabber.send!("<presence to='#{conference}@conference.#{@server}/#{@jid}' />")
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Cirrocumulus
|
2
|
+
#
|
3
|
+
# Cirrocumulus environment. It is a container where all ontologies will be loaded.
|
4
|
+
#
|
5
|
+
class Environment
|
6
|
+
|
7
|
+
def self.current
|
8
|
+
@@instance
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
def initialize(name)
|
14
|
+
puts "Loading #{name} Cirrocumulus environment."
|
15
|
+
|
16
|
+
@name = name
|
17
|
+
@ontologies = []
|
18
|
+
@@instance = self
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Loads ontology instance into environment.
|
23
|
+
#
|
24
|
+
def load_ontology(ontology_instance)
|
25
|
+
puts "Adding #{ontology_instance.name} ontology."
|
26
|
+
@ontologies << ontology_instance
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Runs the environment. It will also restore all loaded ontologies states.
|
31
|
+
#
|
32
|
+
def run
|
33
|
+
@ontologies.each {|ontology| ontology.restore_state }
|
34
|
+
@ontologies.each {|ontology| ontology.run }
|
35
|
+
end
|
36
|
+
|
37
|
+
def join
|
38
|
+
@ontologies.each {|ontology| ontology.join }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
@@instance = nil
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
#
|
2
|
+
# Fact (piece of knowledge) representation.
|
3
|
+
# It holds time when it was observed (added to facts database) and expire time in seconds.
|
4
|
+
#
|
5
|
+
class Fact
|
6
|
+
def initialize(data, time, options)
|
7
|
+
@data = data
|
8
|
+
@time = time
|
9
|
+
@options = options
|
10
|
+
@is_deleted = false
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :data
|
14
|
+
attr_accessor :is_deleted
|
15
|
+
|
16
|
+
def timed_out?
|
17
|
+
return false if @options[:expires] == nil
|
18
|
+
@time + @options[:expires] < Time.now
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Adapter for facts database.
|
24
|
+
#
|
25
|
+
class FactsDatabase
|
26
|
+
def initialize
|
27
|
+
@storage = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def add(fact, options = {})
|
31
|
+
@storage << Fact.new(fact, Time.now, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove(fact)
|
35
|
+
@storage.delete_if {|f| f.data == fact}
|
36
|
+
end
|
37
|
+
|
38
|
+
def enumerate
|
39
|
+
@storage.dup
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class KnowledgeClass
|
44
|
+
class KnowledgeClassDescription
|
45
|
+
attr_accessor :name
|
46
|
+
attr_accessor :primary_key
|
47
|
+
attr_reader :properties
|
48
|
+
|
49
|
+
def initialize(klass)
|
50
|
+
@name = klass.name
|
51
|
+
@properties = []
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
@@classes = {}
|
57
|
+
|
58
|
+
def klass(class_name)
|
59
|
+
@@classes[self.name] = KnowledgeClassDescription.new(self) if @@classes[self.name].nil?
|
60
|
+
@@classes[self.name].name = class_name.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
def id(id_name)
|
64
|
+
@@classes[self.name] = KnowledgeClassDescription.new(self) if @@classes[self.name].nil?
|
65
|
+
@@classes[self.name].primary_key = id_name.to_s
|
66
|
+
@@classes[self.name].properties << id_name.to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
def property(property_name)
|
70
|
+
@@classes[self.name] = KnowledgeClassDescription.new(self) if @@classes[self.name].nil?
|
71
|
+
@@classes[self.name].properties << property_name.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def from_fact(fact)
|
75
|
+
description = @@classes[self.name]
|
76
|
+
return nil if fact[0] != description.name.to_sym
|
77
|
+
|
78
|
+
instance = self.new
|
79
|
+
instance.values[description.primary_key] = fact[1]
|
80
|
+
description.properties.each_with_index do |k, idx|
|
81
|
+
next if idx == 0
|
82
|
+
instance.values[k] = fact[1 + idx*2]
|
83
|
+
end
|
84
|
+
|
85
|
+
instance
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
attr_reader :values
|
90
|
+
|
91
|
+
def initialize
|
92
|
+
@values = {}
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_template
|
96
|
+
description = @@classes[self.class.name]
|
97
|
+
fact = [description.name.to_sym]
|
98
|
+
fact << values[description.primary_key]
|
99
|
+
description.properties.each do |k|
|
100
|
+
next if k == description.primary_key
|
101
|
+
|
102
|
+
fact << k.to_sym
|
103
|
+
fact << k.upcase.to_sym
|
104
|
+
end
|
105
|
+
|
106
|
+
fact
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_fact
|
110
|
+
description = @@classes[self.class.name]
|
111
|
+
fact = [description.name.to_sym]
|
112
|
+
fact << values[description.primary_key]
|
113
|
+
description.properties.each do |k|
|
114
|
+
next if k == description.primary_key
|
115
|
+
|
116
|
+
fact << k.to_sym
|
117
|
+
fact << values[k]
|
118
|
+
end
|
119
|
+
|
120
|
+
fact
|
121
|
+
end
|
122
|
+
|
123
|
+
def method_missing(meth, *args, &block)
|
124
|
+
description = @@classes[self.class.name]
|
125
|
+
description.properties.each do |prop|
|
126
|
+
if meth.to_s == "#{prop}="
|
127
|
+
return values[prop] = args[0]
|
128
|
+
elsif meth.to_s == prop
|
129
|
+
return values[prop]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
super
|
134
|
+
end
|
135
|
+
end
|