cirrocumulus 0.6.3 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|