@@ -11,6 +11,8 @@ module Agents |
||
| 11 | 11 |
|
| 12 | 12 |
The `type` can be one of #{VALID_COMPARISON_TYPES.map { |t| "`#{t}`" }.to_sentence} and compares with the `value`.
|
| 13 | 13 |
|
| 14 |
+ The `value` can be a single value or an array of values. In the case of an array, if one or more values match then the rule matches. |
|
| 15 |
+ |
|
| 14 | 16 |
All rules must match for the Agent to match. The resulting Event will have a payload message of `message`. You can include extractions in the message, for example: `I saw a bar of: <foo.bar>` |
| 15 | 17 |
|
| 16 | 18 |
Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent. |
@@ -49,25 +51,30 @@ module Agents |
||
| 49 | 51 |
incoming_events.each do |event| |
| 50 | 52 |
match = options['rules'].all? do |rule| |
| 51 | 53 |
value_at_path = Utils.value_at(event['payload'], rule['path']) |
| 52 |
- case rule['type'] |
|
| 54 |
+ rule_values = rule['value'] |
|
| 55 |
+ rule_values = [rule_values] unless rule_values.is_a?(Array) |
|
| 56 |
+ |
|
| 57 |
+ match_found = rule_values.any? do |rule_value| |
|
| 58 |
+ case rule['type'] |
|
| 53 | 59 |
when "regex" |
| 54 |
- value_at_path.to_s =~ Regexp.new(rule['value'], Regexp::IGNORECASE) |
|
| 60 |
+ value_at_path.to_s =~ Regexp.new(rule_value, Regexp::IGNORECASE) |
|
| 55 | 61 |
when "!regex" |
| 56 |
- value_at_path.to_s !~ Regexp.new(rule['value'], Regexp::IGNORECASE) |
|
| 62 |
+ value_at_path.to_s !~ Regexp.new(rule_value, Regexp::IGNORECASE) |
|
| 57 | 63 |
when "field>value" |
| 58 |
- value_at_path.to_f > rule['value'].to_f |
|
| 64 |
+ value_at_path.to_f > rule_value.to_f |
|
| 59 | 65 |
when "field>=value" |
| 60 |
- value_at_path.to_f >= rule['value'].to_f |
|
| 66 |
+ value_at_path.to_f >= rule_value.to_f |
|
| 61 | 67 |
when "field<value" |
| 62 |
- value_at_path.to_f < rule['value'].to_f |
|
| 68 |
+ value_at_path.to_f < rule_value.to_f |
|
| 63 | 69 |
when "field<=value" |
| 64 |
- value_at_path.to_f <= rule['value'].to_f |
|
| 70 |
+ value_at_path.to_f <= rule_value.to_f |
|
| 65 | 71 |
when "field==value" |
| 66 |
- value_at_path.to_s == rule['value'].to_s |
|
| 72 |
+ value_at_path.to_s == rule_value.to_s |
|
| 67 | 73 |
when "field!=value" |
| 68 |
- value_at_path.to_s != rule['value'].to_s |
|
| 74 |
+ value_at_path.to_s != rule_value.to_s |
|
| 69 | 75 |
else |
| 70 | 76 |
raise "Invalid type of #{rule['type']} in TriggerAgent##{id}"
|
| 77 |
+ end |
|
| 71 | 78 |
end |
| 72 | 79 |
end |
| 73 | 80 |
|
@@ -4,7 +4,6 @@ require 'date' |
||
| 4 | 4 |
|
| 5 | 5 |
module Agents |
| 6 | 6 |
class WebsiteAgent < Agent |
| 7 |
- cannot_receive_events! |
|
| 8 | 7 |
|
| 9 | 8 |
default_schedule "every_12h" |
| 10 | 9 |
|
@@ -46,6 +45,8 @@ module Agents |
||
| 46 | 45 |
Set `uniqueness_look_back` to limit the number of events checked for uniqueness (typically for performance). This defaults to the larger of #{UNIQUENESS_LOOK_BACK} or #{UNIQUENESS_FACTOR}x the number of detected received results.
|
| 47 | 46 |
|
| 48 | 47 |
Set `force_encoding` to an encoding name if the website does not return a Content-Type header with a proper charset. |
| 48 |
+ |
|
| 49 |
+ The WebsiteAgent can also scrape based on incoming events. It will scrape the url contained in the `url` key of the incoming event payload. |
|
| 49 | 50 |
MD |
| 50 | 51 |
|
| 51 | 52 |
event_description do |
@@ -105,19 +106,23 @@ module Agents |
||
| 105 | 106 |
end |
| 106 | 107 |
|
| 107 | 108 |
def check |
| 108 |
- hydra = Typhoeus::Hydra.new |
|
| 109 | 109 |
log "Fetching #{options['url']}"
|
| 110 |
+ check_url options['url'] |
|
| 111 |
+ end |
|
| 112 |
+ |
|
| 113 |
+ def check_url(in_url) |
|
| 114 |
+ hydra = Typhoeus::Hydra.new |
|
| 110 | 115 |
request_opts = { :followlocation => true }
|
| 111 | 116 |
request_opts[:userpwd] = options['basic_auth'] if options['basic_auth'].present? |
| 112 | 117 |
|
| 113 | 118 |
requests = [] |
| 114 | 119 |
|
| 115 |
- if options['url'].kind_of?(Array) |
|
| 116 |
- options['url'].each do |url| |
|
| 120 |
+ if in_url.kind_of?(Array) |
|
| 121 |
+ in_url.each do |url| |
|
| 117 | 122 |
requests.push(Typhoeus::Request.new(url, request_opts)) |
| 118 | 123 |
end |
| 119 | 124 |
else |
| 120 |
- requests.push(Typhoeus::Request.new(options['url'], request_opts)) |
|
| 125 |
+ requests.push(Typhoeus::Request.new(in_url, request_opts)) |
|
| 121 | 126 |
end |
| 122 | 127 |
|
| 123 | 128 |
requests.each do |request| |
@@ -185,7 +190,7 @@ module Agents |
||
| 185 | 190 |
options['extract'].keys.each do |name| |
| 186 | 191 |
result[name] = output[name][index] |
| 187 | 192 |
if name.to_s == 'url' |
| 188 |
- result[name] = URI.join(options['url'], result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil? |
|
| 193 |
+ result[name] = URI.join(request.base_url, result[name]).to_s if (result[name] =~ URI::DEFAULT_PARSER.regexp[:ABS_URI]).nil? |
|
| 189 | 194 |
end |
| 190 | 195 |
end |
| 191 | 196 |
|
@@ -202,6 +207,13 @@ module Agents |
||
| 202 | 207 |
end |
| 203 | 208 |
end |
| 204 | 209 |
|
| 210 |
+ def receive(incoming_events) |
|
| 211 |
+ incoming_events.each do |event| |
|
| 212 |
+ url_to_scrape = event.payload['url'] |
|
| 213 |
+ check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i |
|
| 214 |
+ end |
|
| 215 |
+ end |
|
| 216 |
+ |
|
| 205 | 217 |
private |
| 206 | 218 |
|
| 207 | 219 |
# This method returns true if the result should be stored as a new event. |
@@ -275,5 +287,7 @@ module Agents |
||
| 275 | 287 |
false |
| 276 | 288 |
end |
| 277 | 289 |
end |
| 290 |
+ |
|
| 278 | 291 |
end |
| 292 |
+ |
|
| 279 | 293 |
end |
@@ -3,37 +3,34 @@ |
||
| 3 | 3 |
|
| 4 | 4 |
Vagrant.configure("2") do |config|
|
| 5 | 5 |
config.omnibus.chef_version = :latest |
| 6 |
- config.vm.define :vb do |vb| |
|
| 7 |
- vb.vm.box = "precise32" |
|
| 8 |
- vb.vm.box_url = "http://files.vagrantup.com/precise32.box" |
|
| 9 |
- vb.vm.network :forwarded_port, host: 3000, guest: 3000 |
|
| 10 | 6 |
|
| 11 |
- vb.vm.provision :chef_solo do |chef| |
|
| 12 |
- chef.roles_path = "roles" |
|
| 13 |
- chef.cookbooks_path = ["cookbooks", "site-cookbooks"] |
|
| 14 |
- chef.add_role("huginn_development")
|
|
| 15 |
- end |
|
| 7 |
+ config.vm.provision :chef_solo do |chef| |
|
| 8 |
+ chef.roles_path = "roles" |
|
| 9 |
+ chef.cookbooks_path = ["cookbooks", "site-cookbooks"] |
|
| 10 |
+ chef.add_role("huginn_development")
|
|
| 11 |
+ # chef.add_role("huginn_production")
|
|
| 16 | 12 |
end |
| 17 | 13 |
|
| 18 |
- config.vm.define :ec2 do |ec2| |
|
| 19 |
- ec2.vm.box = "dummy" |
|
| 20 |
- ec2.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box" |
|
| 14 |
+ config.vm.provider :virtualbox do |vb, override| |
|
| 15 |
+ override.vm.box = "hashicorp/precise64" |
|
| 16 |
+ override.vm.network :forwarded_port, host: 3000, guest: 3000 |
|
| 17 |
+ end |
|
| 18 |
+ |
|
| 19 |
+ config.vm.provider :parallels do |prl, override| |
|
| 20 |
+ override.vm.box = "parallels/ubuntu-12.04" |
|
| 21 |
+ end |
|
| 22 |
+ |
|
| 23 |
+ config.vm.provider :aws do |aws, override| |
|
| 24 |
+ override.vm.box = "dummy" |
|
| 25 |
+ override.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box" |
|
| 21 | 26 |
|
| 22 |
- ec2.vm.provider :aws do |aws, override| |
|
| 23 |
- aws.access_key_id = "" |
|
| 24 |
- aws.secret_access_key = "" |
|
| 25 |
- aws.keypair_name = "" |
|
| 26 |
- aws.region = "us-east-1" |
|
| 27 |
- aws.ami = "ami-d0f89fb9" |
|
| 27 |
+ aws.access_key_id = "" |
|
| 28 |
+ aws.secret_access_key = "" |
|
| 29 |
+ aws.keypair_name = "" |
|
| 30 |
+ aws.region = "us-east-1" |
|
| 31 |
+ aws.ami = "ami-d0f89fb9" |
|
| 28 | 32 |
|
| 29 |
- override.ssh.username = "ubuntu" |
|
| 30 |
- override.ssh.private_key_path = "" |
|
| 31 |
- end |
|
| 32 |
- ec2.vm.provision :chef_solo do |chef| |
|
| 33 |
- chef.roles_path = "roles" |
|
| 34 |
- chef.cookbooks_path = ["cookbooks", "site-cookbooks"] |
|
| 35 |
- chef.add_role("huginn_production")
|
|
| 36 |
- |
|
| 37 |
- end |
|
| 33 |
+ override.ssh.username = "ubuntu" |
|
| 34 |
+ override.ssh.private_key_path = "" |
|
| 38 | 35 |
end |
| 39 | 36 |
end |
@@ -23,6 +23,7 @@ |
||
| 23 | 23 |
"recipe[git]", |
| 24 | 24 |
"recipe[apt]", |
| 25 | 25 |
"recipe[mysql::server]", |
| 26 |
+ "recipe[mysql::client]", |
|
| 26 | 27 |
"recipe[nodejs::install_from_binary]", |
| 27 | 28 |
"recipe[huginn_development]" |
| 28 | 29 |
] |
@@ -16,12 +16,19 @@ group "huginn" do |
||
| 16 | 16 |
action :create |
| 17 | 17 |
end |
| 18 | 18 |
|
| 19 |
-%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libmysqlclient-dev").each do |pkg|
|
|
| 19 |
+%w("ruby1.9.1" "ruby1.9.1-dev" "libxslt-dev" "libxml2-dev" "curl" "libmysqlclient-dev" "rubygems").each do |pkg|
|
|
| 20 | 20 |
package pkg do |
| 21 | 21 |
action :install |
| 22 | 22 |
end |
| 23 | 23 |
end |
| 24 | 24 |
|
| 25 |
+bash "Setting default ruby version to 1.9" do |
|
| 26 |
+ code <<-EOH |
|
| 27 |
+ update-alternatives --set ruby /usr/bin/ruby1.9.1 |
|
| 28 |
+ update-alternatives --set gem /usr/bin/gem1.9.1 |
|
| 29 |
+ EOH |
|
| 30 |
+end |
|
| 31 |
+ |
|
| 25 | 32 |
git "/home/huginn/huginn" do |
| 26 | 33 |
repository 'git://github.com/cantino/huginn.git' |
| 27 | 34 |
reference 'master' |
@@ -48,7 +55,7 @@ bash "huginn dependencies" do |
||
| 48 | 55 |
export LANG="en_US.UTF-8" |
| 49 | 56 |
export LC_ALL="en_US.UTF-8" |
| 50 | 57 |
sudo bundle install |
| 51 |
- sed s/REPLACE_ME_NOW\!/$(sudo rake secret)/ .env.example > .env |
|
| 58 |
+ sed s/REPLACE_ME_NOW\!/$(sudo bundle exec rake secret)/ .env.example > .env |
|
| 52 | 59 |
sudo bundle exec rake db:create |
| 53 | 60 |
sudo bundle exec rake db:migrate |
| 54 | 61 |
sudo bundle exec rake db:seed |
@@ -59,6 +66,6 @@ bash "huginn has been installed and will start in a minute" do |
||
| 59 | 66 |
user "huginn" |
| 60 | 67 |
cwd "/home/huginn/huginn" |
| 61 | 68 |
code <<-EOH |
| 62 |
- sudo foreman start |
|
| 69 |
+ sudo nohup foreman start & |
|
| 63 | 70 |
EOH |
| 64 | 71 |
end |
@@ -71,6 +71,28 @@ describe Agents::TriggerAgent do |
||
| 71 | 71 |
}.should change { Event.count }.by(1)
|
| 72 | 72 |
end |
| 73 | 73 |
|
| 74 |
+ it "handles array of regex" do |
|
| 75 |
+ @event.payload['foo']['bar']['baz'] = "a222b" |
|
| 76 |
+ @checker.options['rules'][0] = {
|
|
| 77 |
+ 'type' => "regex", |
|
| 78 |
+ 'value' => ["a\\db", "a\\Wb"], |
|
| 79 |
+ 'path' => "foo.bar.baz", |
|
| 80 |
+ } |
|
| 81 |
+ lambda {
|
|
| 82 |
+ @checker.receive([@event]) |
|
| 83 |
+ }.should_not change { Event.count }
|
|
| 84 |
+ |
|
| 85 |
+ @event.payload['foo']['bar']['baz'] = "a2b" |
|
| 86 |
+ lambda {
|
|
| 87 |
+ @checker.receive([@event]) |
|
| 88 |
+ }.should change { Event.count }.by(1)
|
|
| 89 |
+ |
|
| 90 |
+ @event.payload['foo']['bar']['baz'] = "a b" |
|
| 91 |
+ lambda {
|
|
| 92 |
+ @checker.receive([@event]) |
|
| 93 |
+ }.should change { Event.count }.by(1)
|
|
| 94 |
+ end |
|
| 95 |
+ |
|
| 74 | 96 |
it "handles negated regex" do |
| 75 | 97 |
@event.payload['foo']['bar']['baz'] = "a2b" |
| 76 | 98 |
@checker.options['rules'][0] = {
|
@@ -89,6 +111,24 @@ describe Agents::TriggerAgent do |
||
| 89 | 111 |
}.should change { Event.count }.by(1)
|
| 90 | 112 |
end |
| 91 | 113 |
|
| 114 |
+ it "handles array of negated regex" do |
|
| 115 |
+ @event.payload['foo']['bar']['baz'] = "a2b" |
|
| 116 |
+ @checker.options['rules'][0] = {
|
|
| 117 |
+ 'type' => "!regex", |
|
| 118 |
+ 'value' => ["a\\db", "a2b"], |
|
| 119 |
+ 'path' => "foo.bar.baz", |
|
| 120 |
+ } |
|
| 121 |
+ |
|
| 122 |
+ lambda {
|
|
| 123 |
+ @checker.receive([@event]) |
|
| 124 |
+ }.should_not change { Event.count }
|
|
| 125 |
+ |
|
| 126 |
+ @event.payload['foo']['bar']['baz'] = "a3b" |
|
| 127 |
+ lambda {
|
|
| 128 |
+ @checker.receive([@event]) |
|
| 129 |
+ }.should change { Event.count }.by(1)
|
|
| 130 |
+ end |
|
| 131 |
+ |
|
| 92 | 132 |
it "puts can extract values into the message based on paths" do |
| 93 | 133 |
@checker.receive([@event]) |
| 94 | 134 |
Event.last.payload['message'].should == "I saw 'a2b' from Joe" |
@@ -109,6 +149,21 @@ describe Agents::TriggerAgent do |
||
| 109 | 149 |
}.should_not change { Event.count }
|
| 110 | 150 |
end |
| 111 | 151 |
|
| 152 |
+ it "handles array of numerical comparisons" do |
|
| 153 |
+ @event.payload['foo']['bar']['baz'] = "5" |
|
| 154 |
+ @checker.options['rules'].first['value'] = [6, 3] |
|
| 155 |
+ @checker.options['rules'].first['type'] = "field<value" |
|
| 156 |
+ |
|
| 157 |
+ lambda {
|
|
| 158 |
+ @checker.receive([@event]) |
|
| 159 |
+ }.should change { Event.count }.by(1)
|
|
| 160 |
+ |
|
| 161 |
+ @checker.options['rules'].first['value'] = [4, 3] |
|
| 162 |
+ lambda {
|
|
| 163 |
+ @checker.receive([@event]) |
|
| 164 |
+ }.should_not change { Event.count }
|
|
| 165 |
+ end |
|
| 166 |
+ |
|
| 112 | 167 |
it "handles exact comparisons" do |
| 113 | 168 |
@event.payload['foo']['bar']['baz'] = "hello world" |
| 114 | 169 |
@checker.options['rules'].first['type'] = "field==value" |
@@ -124,6 +179,21 @@ describe Agents::TriggerAgent do |
||
| 124 | 179 |
}.should change { Event.count }.by(1)
|
| 125 | 180 |
end |
| 126 | 181 |
|
| 182 |
+ it "handles array of exact comparisons" do |
|
| 183 |
+ @event.payload['foo']['bar']['baz'] = "hello world" |
|
| 184 |
+ @checker.options['rules'].first['type'] = "field==value" |
|
| 185 |
+ |
|
| 186 |
+ @checker.options['rules'].first['value'] = ["hello there", "hello universe"] |
|
| 187 |
+ lambda {
|
|
| 188 |
+ @checker.receive([@event]) |
|
| 189 |
+ }.should_not change { Event.count }
|
|
| 190 |
+ |
|
| 191 |
+ @checker.options['rules'].first['value'] = ["hello world", "hello universe"] |
|
| 192 |
+ lambda {
|
|
| 193 |
+ @checker.receive([@event]) |
|
| 194 |
+ }.should change { Event.count }.by(1)
|
|
| 195 |
+ end |
|
| 196 |
+ |
|
| 127 | 197 |
it "handles negated comparisons" do |
| 128 | 198 |
@event.payload['foo']['bar']['baz'] = "hello world" |
| 129 | 199 |
@checker.options['rules'].first['type'] = "field!=value" |
@@ -140,6 +210,22 @@ describe Agents::TriggerAgent do |
||
| 140 | 210 |
}.should change { Event.count }.by(1)
|
| 141 | 211 |
end |
| 142 | 212 |
|
| 213 |
+ it "handles array of negated comparisons" do |
|
| 214 |
+ @event.payload['foo']['bar']['baz'] = "hello world" |
|
| 215 |
+ @checker.options['rules'].first['type'] = "field!=value" |
|
| 216 |
+ @checker.options['rules'].first['value'] = ["hello world", "hello world"] |
|
| 217 |
+ |
|
| 218 |
+ lambda {
|
|
| 219 |
+ @checker.receive([@event]) |
|
| 220 |
+ }.should_not change { Event.count }
|
|
| 221 |
+ |
|
| 222 |
+ @checker.options['rules'].first['value'] = ["hello there", "hello world"] |
|
| 223 |
+ |
|
| 224 |
+ lambda {
|
|
| 225 |
+ @checker.receive([@event]) |
|
| 226 |
+ }.should change { Event.count }.by(1)
|
|
| 227 |
+ end |
|
| 228 |
+ |
|
| 143 | 229 |
it "does fine without dots in the path" do |
| 144 | 230 |
@event.payload = { 'hello' => "world" }
|
| 145 | 231 |
@checker.options['rules'].first['type'] = "field==value" |
@@ -331,6 +331,19 @@ describe Agents::WebsiteAgent do |
||
| 331 | 331 |
end |
| 332 | 332 |
end |
| 333 | 333 |
end |
| 334 |
+ |
|
| 335 |
+ describe "#receive" do |
|
| 336 |
+ it "should scrape from the url element in incoming event payload" do |
|
| 337 |
+ @event = Event.new |
|
| 338 |
+ @event.agent = agents(:bob_rain_notifier_agent) |
|
| 339 |
+ @event.payload = { 'url' => "http://xkcd.com" }
|
|
| 340 |
+ |
|
| 341 |
+ lambda {
|
|
| 342 |
+ @checker.options = @site |
|
| 343 |
+ @checker.receive([@event]) |
|
| 344 |
+ }.should change { Event.count }.by(1)
|
|
| 345 |
+ end |
|
| 346 |
+ end |
|
| 334 | 347 |
end |
| 335 | 348 |
|
| 336 | 349 |
describe "checking with http basic auth" do |
@@ -361,4 +374,4 @@ describe Agents::WebsiteAgent do |
||
| 361 | 374 |
end |
| 362 | 375 |
end |
| 363 | 376 |
end |
| 364 |
-end |
|
| 377 |
+end |