├── app.yaml ├── guppy ├── func.yaml ├── Gemfile ├── README.md ├── slack.payload ├── guppy.rb └── commands.json ├── aws_cost_bot ├── .gitignore ├── Gemfile ├── slack.payload ├── config-example.json ├── README.md ├── Gemfile.lock └── costbot.rb ├── dicey ├── Gemfile ├── README.md ├── slack.payload ├── dicey.rb └── Gemfile.lock ├── curlbot ├── Gemfile ├── README.md ├── slack.payload ├── curlbot.rb └── Gemfile.lock ├── echobot ├── Gemfile ├── README.md ├── slack.payload ├── echobot.rb └── Gemfile.lock ├── hellobot ├── Gemfile ├── README.md ├── slack.payload ├── hellobot.rb └── Gemfile.lock ├── worker ├── Gemfile ├── slack.payload ├── README.md ├── Gemfile.lock └── worker.rb ├── .gitignore ├── salesforce ├── oppbot-screenshot.png ├── Gemfile ├── config-example.json ├── README.md ├── Gemfile.lock ├── google_datastore.rb └── opportunity_bot.rb ├── tubey ├── Gemfile ├── README.md ├── slack.payload ├── tubey.rb └── Gemfile.lock ├── votey ├── Gemfile ├── slack.payload ├── README.md ├── Gemfile.lock └── votey.rb ├── uptime ├── sample_config.json ├── slack.go ├── README.md ├── main.go └── pingdom.go └── README.md /app.yaml: -------------------------------------------------------------------------------- 1 | name: slackbots 2 | -------------------------------------------------------------------------------- /guppy/func.yaml: -------------------------------------------------------------------------------- 1 | version: 0.0.14 2 | runtime: ruby 3 | entrypoint: ruby guppy.rb 4 | -------------------------------------------------------------------------------- /aws_cost_bot/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | costs.csv 3 | tmp.rb 4 | tmp2.rb 5 | ri-coverage.csv 6 | -------------------------------------------------------------------------------- /dicey/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'slack_webhooks' 4 | gem 'iron_worker' 5 | -------------------------------------------------------------------------------- /curlbot/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'iron_worker' 4 | gem 'slack_webhooks' 5 | -------------------------------------------------------------------------------- /echobot/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'slack_webhooks' 4 | gem 'iron_worker' 5 | -------------------------------------------------------------------------------- /hellobot/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'iron_worker' 4 | gem 'slack_webhooks' 5 | -------------------------------------------------------------------------------- /hellobot/README.md: -------------------------------------------------------------------------------- 1 | This will simply reply with a `Hello @user!`. The simplest bot you can get. 2 | -------------------------------------------------------------------------------- /guppy/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'slack_webhooks', '>= 0.2.1' 4 | gem 'json', '>2.0.0' 5 | -------------------------------------------------------------------------------- /worker/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'slack_webhooks' 4 | gem 'iron_worker', '>= 3.0.2' 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | 4 | *.zip 5 | config.json 6 | iron.json 7 | 8 | bundle/ 9 | .bundle/ 10 | -------------------------------------------------------------------------------- /salesforce/oppbot-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeder/slackbots/HEAD/salesforce/oppbot-screenshot.png -------------------------------------------------------------------------------- /tubey/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'slack_webhooks' 4 | gem 'youtube_it' 5 | gem 'iron_worker' 6 | -------------------------------------------------------------------------------- /votey/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'slack_webhooks' 4 | gem 'iron_worker' 5 | gem 'iron_cache' 6 | -------------------------------------------------------------------------------- /dicey/README.md: -------------------------------------------------------------------------------- 1 | Roll the dice in slack. 2 | 3 | /roll [sides] 4 | 5 | Will post a number from 1-sides back to Slack. 6 | -------------------------------------------------------------------------------- /echobot/README.md: -------------------------------------------------------------------------------- 1 | This one is a bit different, make an Outgoing Webhook in Slack rather than a Slash Command. 2 | Trigger it with `echo`. 3 | -------------------------------------------------------------------------------- /tubey/README.md: -------------------------------------------------------------------------------- 1 | This is just like /giphy for videos. Will choose a random Youtube video. 2 | 3 | Example: 4 | 5 | ``` 6 | /tubey penguins 7 | ``` 8 | -------------------------------------------------------------------------------- /curlbot/README.md: -------------------------------------------------------------------------------- 1 | Run any curl command and post response back to slack. 2 | 3 | Examples: 4 | 5 | ``` 6 | /curl http://mq-v3-aws-us-east-1.iron.io 7 | ``` 8 | -------------------------------------------------------------------------------- /salesforce/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'restforce' 4 | gem 'slack_webhooks' 5 | gem 'intercom' 6 | gem 'iron_worker' 7 | gem 'iron_cache' 8 | -------------------------------------------------------------------------------- /aws_cost_bot/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'slack_webhooks' 4 | gem 'iron_worker' 5 | gem 'iron_cache' 6 | gem 'fog' 7 | gem 'amazon-pricing' 8 | gem 'slack-api' 9 | -------------------------------------------------------------------------------- /dicey/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=directmessage&user_id=someid&user_name=travis&command=%2Fhello&text=6 2 | -------------------------------------------------------------------------------- /echobot/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=random&user_id=someid&user_name=travis&text=echo+test&trigger_word=echo -------------------------------------------------------------------------------- /tubey/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=directmessage&user_id=someid&user_name=travis&command=%2Ftubey&text=shrug 2 | -------------------------------------------------------------------------------- /hellobot/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=directmessage&user_id=someid&user_name=travis&command=%2Fhello&text=penguins 2 | -------------------------------------------------------------------------------- /votey/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=directmessage&user_id=someid&user_name=travis&command=%2Fhello&text=vote1 no 2 | -------------------------------------------------------------------------------- /aws_cost_bot/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=directmessage&user_id=someid&user_name=travis&command=%2Fhello&text=vote1 no 2 | -------------------------------------------------------------------------------- /worker/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=random&user_id=someid&user_name=travis&command=%2Fworker&text=IRON_PROJECT_ID IRON_TOKEN hello -------------------------------------------------------------------------------- /curlbot/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=#test&user_id=someid&user_name=travis&command=%2Fcurl&text=http://mq-aws-us-east-1-2.iron.io/version 2 | -------------------------------------------------------------------------------- /aws_cost_bot/config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "webhook_url": "https://hooks.slack.com/services/ABC", 3 | "slack_token": "abc", 4 | "channel": "#test", 5 | "aws": { 6 | "access_key": "ACCESS KEY", 7 | "secret_key": "SECRET KEY" 8 | } 9 | } -------------------------------------------------------------------------------- /hellobot/hellobot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'iron_worker' 3 | require 'slack_webhooks' 4 | 5 | sh = SlackWebhooks::Hook.new('hellobot', IronWorker.payload, IronWorker.config['webhook_url']) 6 | text = "Hello #{sh.user_name}!" 7 | sh.send(text) 8 | -------------------------------------------------------------------------------- /guppy/README.md: -------------------------------------------------------------------------------- 1 | # Guppy - the predefined giphy 2 | 3 | Kind of like /giphy, but you can set your own keywords and responses. See commands.json to change it. 4 | 5 | If you want your own commands.json, change guppy.rb to use the file loading, by default it reads it 6 | directly from this repo on Github. 7 | -------------------------------------------------------------------------------- /guppy/slack.payload: -------------------------------------------------------------------------------- 1 | token=somerandomtoken&team_id=someteamid&team_domain=somename&channel_id=somechannelid&channel_name=directmessage&user_id=someid&user_name=travis&command=%2Fguppy&text=this%20is%20the%20body&response_url=https://hooks.slack.com/commands/1234/5678&trigger_id=13345224609.738474920.8088930838d88f008e0 2 | -------------------------------------------------------------------------------- /votey/README.md: -------------------------------------------------------------------------------- 1 | Users can vote on something. 2 | 3 | ``` 4 | /vote yes/no 5 | ``` 6 | 7 | ## Additional steps required to run this Bot 8 | 9 | This bot uses [IronCache](http://www.iron.io/cache) to store results temporarily. 10 | Create an iron.json file in this directory with your Iron.io project_id and token. 11 | -------------------------------------------------------------------------------- /uptime/sample_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "PINGDOM_EMAIL", 3 | "password": "PINGDOM_PASSWORD", 4 | "api_key": "PINGDOM_API_KEY", 5 | "webook_url": "SLACK_WEBHOOK_URL", 6 | "mq":[ 7 | 123, 8 | 456 9 | ], 10 | "worker": [ 11 | 123, 12 | 456 13 | ], 14 | "other": [ 15 | 123, 16 | 456 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /echobot/echobot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'uri' 3 | require 'slack-notifier' 4 | require 'iron_worker' 5 | require 'slack_webhooks' 6 | 7 | sh = SlackWebhooks::Hook.new('echobot', IronWorker.payload, IronWorker.config['webhook_url']) 8 | 9 | # Slice off the 'echo' text 10 | text = sh.text.split(' ')[1..-1].join(' ') 11 | 12 | sh.send(text) 13 | -------------------------------------------------------------------------------- /worker/README.md: -------------------------------------------------------------------------------- 1 | Runs an IronWorker, returns results to Slack. 2 | 3 | Example: 4 | 5 | ``` 6 | /worker IRON_PROJECT_ID IRON_TOKEN MY_WORKER_NAME 7 | ``` 8 | 9 | ## Testing 10 | 11 | This one requires a change to the `slack.payload` file in this directory to include your [Iron.io](http://www.iron.io) 12 | credentials. Replace `IRON_PROJECT_ID` and `IRON_TOKEN` to your Iron credentials then test with the Docker run command. 13 | 14 | -------------------------------------------------------------------------------- /echobot/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | iron_core (1.0.7) 5 | rest (>= 3.0.4) 6 | iron_worker (3.0.2) 7 | iron_core (>= 0.5.1) 8 | rest (>= 3.0.6) 9 | net-http-persistent (2.9.4) 10 | netrc (0.10.3) 11 | rest (3.0.6) 12 | net-http-persistent (>= 2.9.1) 13 | netrc 14 | slack-notifier (1.1.0) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | iron_worker 21 | slack-notifier 22 | -------------------------------------------------------------------------------- /dicey/dicey.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'iron_worker' 3 | require 'slack_webhooks' 4 | 5 | sh = SlackWebhooks::Hook.new('dicey', IronWorker.payload, IronWorker.config['webhook_url']) 6 | sh.icon_url = "http://www.psdgraphics.com/file/red-dice-icon.jpg" 7 | max = 6 8 | if sh.text != nil && sh.text != "" 9 | max = sh.text.to_i 10 | if max <= 0 11 | max = 6 12 | end 13 | end 14 | text = "#{sh.username} rolled a *#{1 + rand(max)}*" 15 | sh.send(text) 16 | -------------------------------------------------------------------------------- /salesforce/config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "webhook_url": "https://hooks.slack.com/services/ABC", 3 | "channel": "#sales", 4 | "salesforce": { 5 | "description": "This is using restforce gem, see here for more information on these value: https://github.com/ejholmes/restforce", 6 | "username": "sfusername@yourdomain.com", 7 | "password": "sfpassword", 8 | "security_token": "sfsecuritytoken", 9 | "client_id": "sfclientid", 10 | "client_secret": "sfclientsecret" 11 | } 12 | } -------------------------------------------------------------------------------- /curlbot/curlbot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'iron_worker' 3 | require 'slack_webhooks' 4 | 5 | sh = SlackWebhooks::Hook.new('curlbot', IronWorker.payload, IronWorker.config['webhook_url']) 6 | 7 | s = `curl --silent #{sh.text}` 8 | puts s 9 | 10 | attachment = { 11 | "fallback" => s, 12 | "title" => "Response:", 13 | # "title_link" => "somelink", 14 | # "text" => "```pre\n#{s}\n```", 15 | "text" => s, 16 | } 17 | 18 | sh.send("/curl #{sh.text}", attachment: attachment) 19 | -------------------------------------------------------------------------------- /dicey/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | iron_core (1.0.7) 5 | rest (>= 3.0.4) 6 | iron_worker (3.0.2) 7 | iron_core (>= 0.5.1) 8 | rest (>= 3.0.6) 9 | net-http-persistent (2.9.4) 10 | netrc (0.10.3) 11 | rest (3.0.6) 12 | net-http-persistent (>= 2.9.1) 13 | netrc 14 | slack-notifier (1.1.0) 15 | slack_webhooks (0.0.4) 16 | slack-notifier (>= 0.5.1) 17 | 18 | PLATFORMS 19 | ruby 20 | 21 | DEPENDENCIES 22 | iron_worker 23 | slack_webhooks 24 | -------------------------------------------------------------------------------- /curlbot/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | iron_core (1.0.7) 5 | rest (>= 3.0.4) 6 | iron_worker (3.1.0) 7 | iron_core (>= 0.5.1) 8 | rest (>= 3.0.6) 9 | net-http-persistent (2.9.4) 10 | netrc (0.10.3) 11 | rest (3.0.6) 12 | net-http-persistent (>= 2.9.1) 13 | netrc 14 | slack-notifier (1.2.1) 15 | slack_webhooks (0.0.10) 16 | slack-notifier (>= 0.5.1) 17 | 18 | PLATFORMS 19 | ruby 20 | 21 | DEPENDENCIES 22 | iron_worker 23 | slack_webhooks 24 | -------------------------------------------------------------------------------- /worker/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | iron_core (1.0.7) 5 | rest (>= 3.0.4) 6 | iron_worker (3.0.2) 7 | iron_core (>= 0.5.1) 8 | rest (>= 3.0.6) 9 | net-http-persistent (2.9.4) 10 | netrc (0.10.3) 11 | rest (3.0.6) 12 | net-http-persistent (>= 2.9.1) 13 | netrc 14 | slack-notifier (1.2.1) 15 | slack_webhooks (0.0.10) 16 | slack-notifier (>= 0.5.1) 17 | 18 | PLATFORMS 19 | ruby 20 | 21 | DEPENDENCIES 22 | iron_worker (>= 3.0.2) 23 | slack_webhooks 24 | -------------------------------------------------------------------------------- /salesforce/README.md: -------------------------------------------------------------------------------- 1 | ## Opportunity Bot 2 | 3 | Posts updates to Salesforce Opportunities to the Slack Channel of your choice. 4 | 5 | ![Oppbot Screenshot](oppbot-screenshot.png) 6 | 7 | This one is meant to be scheduled, not to be used as a slash command. After uploading, go into HUD 8 | and schedule it to run every hour. 9 | 10 | ## Additional steps required to run this Bot 11 | 12 | This bot uses [IronCache](http://www.iron.io/cache) to store results temporarily. 13 | Create an iron.json file in this directory with your Iron.io project_id and token. 14 | -------------------------------------------------------------------------------- /hellobot/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | iron_core (1.0.7) 5 | rest (>= 3.0.4) 6 | iron_worker (3.0.1) 7 | iron_core (>= 0.5.1) 8 | rest (>= 3.0.6) 9 | net-http-persistent (2.9.4) 10 | netrc (0.10.3) 11 | rest (3.0.6) 12 | net-http-persistent (>= 2.9.1) 13 | netrc 14 | slack-notifier (1.1.0) 15 | slack_webhooks (0.0.1) 16 | slack-notifier (>= 0.5.1) 17 | 18 | PLATFORMS 19 | ruby 20 | 21 | DEPENDENCIES 22 | iron_worker 23 | slack-notifier 24 | slack_webhooks 25 | -------------------------------------------------------------------------------- /votey/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | iron_cache (1.4.2) 5 | iron_core (>= 0.5.1) 6 | iron_core (1.0.7) 7 | rest (>= 3.0.4) 8 | iron_worker (3.0.2) 9 | iron_core (>= 0.5.1) 10 | rest (>= 3.0.6) 11 | net-http-persistent (2.9.4) 12 | netrc (0.10.3) 13 | rest (3.0.6) 14 | net-http-persistent (>= 2.9.1) 15 | netrc 16 | slack-notifier (1.1.0) 17 | slack_webhooks (0.0.10) 18 | slack-notifier (>= 0.5.1) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | iron_cache 25 | iron_worker 26 | slack_webhooks 27 | -------------------------------------------------------------------------------- /tubey/tubey.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'youtube_it' 3 | require 'iron_worker' 4 | require 'slack_webhooks' 5 | 6 | sh = SlackWebhooks::Hook.new('tubey', IronWorker.payload, IronWorker.config['webhook_url']) 7 | sh.icon_url = 'https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2015-03-10/3999143873_e2fb4ca39b4876bb0bf2_48.jpg' 8 | 9 | client = YouTubeIt::Client.new 10 | vids = client.videos_by(:query => sh.text, :safe_search => "strict") 11 | # p vids 12 | puts "count: #{vids.total_result_count}" 13 | vids.videos.each do |v| 14 | puts "name: #{v.title}" 15 | end 16 | 17 | # Video object attributes: https://github.com/kylejginavan/youtube_it/blob/master/lib/youtube_it/model/video.rb 18 | v = vids.videos.sample 19 | puts "sample: #{v.title} #{v.player_url}" 20 | 21 | sh.send("#{sh.command} #{sh.text}\n#{v.player_url}") 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slackbots for Fn Project 2 | 3 | All of the examples in this repository follow the same pattern for testing and uploading to [Fn](https://fnproject.io). Assuming 4 | you have an Fn server running and the CLI installed, just `cd` 5 | into the bots directory and do the following: 6 | 7 | ## Deploy 8 | 9 | ```sh 10 | fn deploy --app slackbots BOTNAME 11 | ``` 12 | 13 | ## Create a Slash command in Slack 14 | 15 | If you don't have a Slack app already, [start here](https://api.slack.com/apps). 16 | 17 | In your app, click `Slash Commands` and create one that points to your deployed bot/function. 18 | 19 | ## Install the app into your team 20 | 21 | You'll see `Install App` on the left side of your app in the Slack console. Click that and follow the directions. 22 | 23 | ### 6) Try it out! 24 | 25 | Now in slack, type `/ [options]` and you'll see the magic! 26 | -------------------------------------------------------------------------------- /salesforce/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | faraday (0.9.1) 5 | multipart-post (>= 1.2, < 3) 6 | faraday_middleware (0.9.1) 7 | faraday (>= 0.7.4, < 0.10) 8 | hashie (3.4.1) 9 | intercom (2.4.4) 10 | json (~> 1.8) 11 | iron_cache (1.4.2) 12 | iron_core (>= 0.5.1) 13 | iron_core (1.0.7) 14 | rest (>= 3.0.4) 15 | iron_worker (3.0.2) 16 | iron_core (>= 0.5.1) 17 | rest (>= 3.0.6) 18 | json (1.8.2) 19 | multipart-post (2.0.0) 20 | net-http-persistent (2.9.4) 21 | netrc (0.10.3) 22 | rest (3.0.6) 23 | net-http-persistent (>= 2.9.1) 24 | netrc 25 | restforce (1.5.1) 26 | faraday (~> 0.9.0) 27 | faraday_middleware (>= 0.8.8) 28 | hashie (>= 1.2.0, < 4.0) 29 | json (>= 1.7.5, < 1.9.0) 30 | slack-notifier (1.1.0) 31 | slack_webhooks (0.0.10) 32 | slack-notifier (>= 0.5.1) 33 | 34 | PLATFORMS 35 | ruby 36 | 37 | DEPENDENCIES 38 | intercom 39 | iron_cache 40 | iron_worker 41 | restforce 42 | slack_webhooks 43 | -------------------------------------------------------------------------------- /aws_cost_bot/README.md: -------------------------------------------------------------------------------- 1 | Given a set of AWS credential, this bot will go check all your servers and the costs associated with them, aggregated by Name. 2 | 3 | ## Additional steps required to run this Bot 4 | 5 | ### 1) Make an iron.json file 6 | 7 | This bot uses [IronCache](http://www.iron.io/cache) to store results temporarily. 8 | Create an iron.json file in this directory with your Iron.io project_id and token. 9 | 10 | ### 2) Add your AWS credentials to config.json 11 | 12 | Copy `config-example.json` to `config.json` and fill it in. 13 | 14 | ### 3) Test it 15 | 16 | ``` 17 | docker run --rm -v "$(pwd)":/worker -w /worker iron/images:ruby-2.1 sh -c 'ruby costbot.rb -payload slack.payload -config config.json' 18 | ``` 19 | 20 | ### 4) Upload it 21 | 22 | ``` 23 | zip -r costbot.zip . 24 | iron worker upload --name costbot --zip costbot.zip iron/images:ruby-2.1 ruby costbot.rb 25 | ``` 26 | 27 | ### 5) Schedule it 28 | 29 | This bot is intended to be scheduled, rather than turned into a slash command so instead of making a slash command, 30 | schedule it. After you upload, you'll see a link on your console to HUD where you can schedule it in the 31 | UI or you can use the command below: 32 | 33 | ``` 34 | iron worker schedule --start-at 2015-06-24T08:00:00Z --run-every 86400 --payload-file slack.payload costbot 35 | ``` 36 | -------------------------------------------------------------------------------- /tubey/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | builder (3.2.2) 5 | excon (0.44.4) 6 | faraday (0.9.1) 7 | multipart-post (>= 1.2, < 3) 8 | iron_core (1.0.7) 9 | rest (>= 3.0.4) 10 | iron_worker (3.0.1) 11 | iron_core (>= 0.5.1) 12 | rest (>= 3.0.6) 13 | json (1.8.2) 14 | jwt (1.4.1) 15 | mini_portile (0.6.2) 16 | multi_json (1.11.0) 17 | multi_xml (0.5.5) 18 | multipart-post (2.0.0) 19 | net-http-persistent (2.9.4) 20 | netrc (0.10.3) 21 | nokogiri (1.6.6.2) 22 | mini_portile (~> 0.6.0) 23 | oauth (0.4.7) 24 | oauth2 (1.0.0) 25 | faraday (>= 0.8, < 0.10) 26 | jwt (~> 1.0) 27 | multi_json (~> 1.3) 28 | multi_xml (~> 0.5) 29 | rack (~> 1.2) 30 | rack (1.6.0) 31 | rest (3.0.6) 32 | net-http-persistent (>= 2.9.1) 33 | netrc 34 | simple_oauth (0.3.1) 35 | slack-notifier (1.1.0) 36 | slack_webhooks (0.0.4) 37 | slack-notifier (>= 0.5.1) 38 | youtube_it (2.4.2) 39 | builder 40 | excon 41 | faraday (>= 0.8, < 0.10) 42 | json (~> 1.8) 43 | nokogiri (~> 1.6.0) 44 | oauth (~> 0.4.4) 45 | oauth2 (~> 1.0.0) 46 | simple_oauth (>= 0.1.5) 47 | 48 | PLATFORMS 49 | ruby 50 | 51 | DEPENDENCIES 52 | iron_worker 53 | slack_webhooks 54 | youtube_it 55 | -------------------------------------------------------------------------------- /uptime/slack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | type slackC struct { 11 | url string 12 | } 13 | 14 | type slackPayload struct { 15 | Text string `json:"text"` 16 | Attachments []Attachment `json:"attachments"` 17 | } 18 | 19 | type Attachment struct { 20 | Fallback string `json:"fallback"` 21 | Color string `json:"color"` 22 | Pretext string `json:"pretext"` 23 | AuthorName string `json:"author_name"` 24 | AuthorLink string `json:"author_link"` 25 | AuthorIcon string `json:"author_icon"` 26 | Title string `json:"title"` 27 | TitleLink string `json:"title_link"` 28 | Text string `json:"text"` 29 | Fields []Field `json:"fields"` 30 | ImageURL string `json:"image_url"` 31 | ThumbURL string `json:"thumb_url"` 32 | } 33 | type Field struct { 34 | Title string `json:"title"` 35 | Value string `json:"value"` 36 | Short bool `json:"short"` 37 | } 38 | 39 | func (s *slackC) post(text string, attachments []Attachment) { 40 | payload := slackPayload{Text: text, Attachments: attachments} 41 | b, err := json.Marshal(payload) 42 | if err != nil { 43 | fmt.Println(err) 44 | return 45 | } 46 | buf := bytes.NewBuffer(b) 47 | _, err = http.Post(s.url, "application/json", buf) 48 | if err != nil { 49 | fmt.Println(err) 50 | return 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /guppy/guppy.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'slack_webhooks' 3 | require 'json' 4 | 5 | # Comment out the next line and uncomment the one after to load the commands.json from your local file 6 | # rather than the URL 7 | responses = JSON.load(open('https://raw.githubusercontent.com/treeder/slackbots/master/guppy/commands.json')) 8 | # Use this one to load from file: responses = JSON.load(File.open('commands.json')) 9 | 10 | payload = STDIN.read 11 | STDERR.puts payload 12 | if payload == "" 13 | # then probably just testing 14 | puts "Need a payload from Slack... :(" 15 | return 16 | end 17 | 18 | sh = SlackWebhooks::Hook.new('guppy', payload, nil) 19 | 20 | attachment = { 21 | "fallback" => "wat?!", # "(╯°□°)╯︵ ┻━┻)", 22 | "image_url" => "http://i.imgur.com/7kZ562z.jpg" 23 | } 24 | 25 | r = responses[sh.text] 26 | if r 27 | attachment['image_url'] = r['image_url'] 28 | else 29 | # help 30 | help = "Available options are:\n" 31 | responses.each_key { |k| help << "* #{k}\n" } 32 | response = { 33 | "response_type" => "ephemeral", 34 | "text" => help, 35 | "attachments" => [attachment] 36 | } 37 | s = response.to_json 38 | STDERR.puts "responding with #{s}" 39 | puts s 40 | exit 41 | end 42 | 43 | s = "#{sh.command} #{sh.text}" 44 | response = { 45 | "response_type" => "in_channel", 46 | "text" => s, 47 | "attachments" => [attachment] 48 | } 49 | puts response.to_json 50 | -------------------------------------------------------------------------------- /uptime/README.md: -------------------------------------------------------------------------------- 1 | # Uptime bot 2 | 3 | Uptime bot uses pingdom checks to determing the total uptime for all services. Uptime is an aggregate, not an average. 4 | Pingdom requires your username, password, and api token in order to use their api. Due to the amount of redundant checks we have, this version requires you to supply the ids of the checks you want to use. In our specific case, we have three different sets: mq checks, worker checks, and other services. 5 | 6 | ## How to use 7 | 8 | #### Put your pingdom username, password, and api token in the config file. 9 | ```json 10 | { 11 | "username": "USERNAME", 12 | "password": "PASSWORD", 13 | "api_token": "API_TOKEN" 14 | ``` 15 | 16 | #### Put the slack webhook url you want to use 17 | ```json 18 | "webhook_url": "WEBHOOKURL" 19 | ``` 20 | 21 | #### Put in the ids of the checks you want to use. 22 | ```json 23 | "mq": [ 24 | 123, 25 | 123 26 | ], 27 | "worker": [ 28 | 123, 29 | 123 30 | ], 31 | "other": [ 32 | 123, 33 | 123 34 | ] 35 | } 36 | ``` 37 | #### Build the bot using docker 38 | ``` 39 | docker run --rm -v "$GOPATH":/gopath -e "GOPATH=/gopath" -v "$(pwd)":/worker -w /worker iron/images:go-1.4 sh -c 'go build -o uptime' 40 | ``` 41 | 42 | ## Uploading to iron worker 43 | #### Zip the folder 44 | ``` 45 | zip -r uptime.zip . 46 | ``` 47 | 48 | #### Upload to ironworker 49 | ``` 50 | iron worker upload --zip uptime.zip --name uptime iron/images:go-1.4 ./uptime 51 | ``` 52 | 53 | #### Run the worker 54 | ``` 55 | iron worker queue uptime 56 | ``` 57 | -------------------------------------------------------------------------------- /worker/worker.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'iron_worker' 3 | require 'slack_webhooks' 4 | 5 | project_id = nil 6 | token = nil 7 | worker_name = nil 8 | payload_for_worker = nil 9 | send_usage = false 10 | 11 | sh = SlackWebhooks::Hook.new('worker', IronWorker.payload, IronWorker.config['webhook_url']) 12 | ts = sh.text.split(' ') 13 | if ts.length < 3 14 | send_usage = true 15 | else 16 | project_id = ts[0] 17 | token = ts[1] 18 | worker_name = ts[2] 19 | payload = ts[3..-1].join(' ') 20 | end 21 | 22 | puts "project_id=#{project_id} token=#{token} worker_name=#{worker_name} payload=#{payload_for_worker}" 23 | 24 | if send_usage 25 | puts "invalid params passed in, sending usage" 26 | sh.channel = sh.username 27 | sh.send("Usage: /worker ") 28 | exit 29 | end 30 | 31 | puts "all good queuing task" 32 | wc = IronWorker::Client.new(project_id: project_id, token: token) 33 | task = wc.tasks.create(worker_name, payload) 34 | puts "task_id=#{task.id}" 35 | wc.tasks.wait_for(task.id) do |t| 36 | puts t.msg 37 | end 38 | log = wc.tasks.log(task.id) 39 | puts log 40 | 41 | attachment = { 42 | "fallback" => "wat?!", 43 | "title" => "Worker '#{worker_name}' executed", 44 | "title_link" => "https://hud.iron.io/tq/projects/#{project_id}/tasks/#{task.id}", 45 | "text" => log, 46 | # "image_url" => "http://i.imgur.com/7kZ562z.jpg" 47 | # "color": "#764FA5" 48 | } 49 | 50 | sh.send("/worker #{project_id} #{worker_name} #{payload}", attachment: attachment) 51 | -------------------------------------------------------------------------------- /votey/votey.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'iron_worker' 3 | require 'slack_webhooks' 4 | require 'iron_cache' 5 | 6 | sh = SlackWebhooks::Hook.new('votey', IronWorker.payload, IronWorker.config['webhook_url']) 7 | sh.icon_url = "http://s2.hubimg.com/u/7842695_f260.jpg" 8 | sh.set_usage "Usage: /vote " 9 | exit if sh.help? 10 | 11 | @ic = IronCache::Client.new 12 | @cache = @ic.cache("votey") 13 | 14 | expires_in = 86400 15 | split = sh.text.split(' ') 16 | if split.length < 2 17 | sh.send_usage("Invalid parameters.") 18 | exit 19 | end 20 | votename = split[0] 21 | if split[1][0] != 'y' && split[1][0] != 'n' 22 | sh.send_usage("Must be yes or no") 23 | exit 24 | end 25 | # Ok, input looks ok, let's continue 26 | 27 | yes = split[1][0] == 'y' 28 | # Store what the user posted so we don't count it twice 29 | users_vote_key = "#{votename}-user:#{sh.username}" 30 | item = @cache.get(users_vote_key) 31 | changed = false 32 | already_voted = false 33 | if item 34 | already_voted = true 35 | # then user already voted, if it changed, we can change the counts 36 | if item.value[0] == split[1][0] 37 | # Same so do nothing 38 | else 39 | changed = true 40 | # change votes 41 | end 42 | end 43 | yinc = 0 44 | ninc = 0 45 | @cache.put(users_vote_key, split[1], :expires_in => expires_in) 46 | if already_voted 47 | if changed 48 | # need to update both 49 | if yes 50 | yinc = 1 51 | ninc = -1 52 | else 53 | yinc = -1 54 | ninc = 1 55 | end 56 | end 57 | else 58 | if yes 59 | yinc = 1 60 | else 61 | ninc = 1 62 | end 63 | end 64 | 65 | yeskey = "#{votename}-yes" 66 | nokey = "#{votename}-no" 67 | yr = nil 68 | nr = nil 69 | yeses = 0 70 | nos = 0 71 | if yinc != 0 72 | yr = @cache.increment(yeskey, yinc, :expires_in => expires_in) 73 | end 74 | if ninc != 0 75 | nr = @cache.increment(nokey, ninc, :expires_in => expires_in) 76 | end 77 | if yr.nil? 78 | yr = @cache.get(yeskey) 79 | end 80 | if nr.nil? 81 | nr = @cache.get(nokey) 82 | end 83 | if !yr.nil? 84 | yeses = yr.value 85 | end 86 | if !nr.nil? 87 | nos = nr.value 88 | end 89 | puts "yeses: #{yeses}" 90 | puts "nos: #{nos}" 91 | 92 | text = "" 93 | color = "warning" 94 | if yeses > nos 95 | color = "good" 96 | elsif yeses < nos 97 | color = "danger" 98 | end 99 | 100 | attachment = { 101 | "fallback" => text, 102 | "text" => "Voting results for `/vote #{votename}`", 103 | "color" => color, 104 | "mrkdwn_in" => ["text", "pretext"], 105 | "fields" => [ 106 | { 107 | "title" => "Yes", 108 | "value" => "#{yeses}", 109 | "short" => true 110 | }, 111 | { 112 | "title" => "No", 113 | "value" => "#{nos}", 114 | "short" => true 115 | }, 116 | ] 117 | } 118 | 119 | sh.send(text, attachment: attachment) 120 | -------------------------------------------------------------------------------- /guppy/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "tableflip": {"image_url": "http://i.imgur.com/oGwPRux.png"}, 3 | "shrug": {"image_url": "http://www.theblaze.com/wp-content/uploads/2015/03/Google-comment-gif.gif"}, 4 | "forlorn": {"image_url": "https://lh4.googleusercontent.com/-Jp85Ek_5jJc/UdEYqhHzuLI/AAAAAAACF3s/U708yywLDCQ/w700-h412-no/forlorn%2Bcat.gif"}, 5 | "beardscratch": {"image_url": "https://lh5.googleusercontent.com/-gfUwQSvd1bs/UjjHeU-b6HI/AAAAAAAC81o/xtMpBn1F87E/w958-h539-no/beardman.gif"}, 6 | "atlast": {"image_url": "https://lh4.googleusercontent.com/-jzywmNWCPps/UdEYqnq2ECI/AAAAAAACF3w/eNnS_KXHzN0/w480-h226-no/morpheus-at-last.gif"}, 7 | "ohyeah": {"image_url": "https://lh4.googleusercontent.com/-IqiWUKEzd_U/UnmdxBZp-bI/AAAAAAADFf4/l7dyXzMTdDs/w245-h285-no/code-05.gif"}, 8 | "facepalm": {"image_url": "https://lh4.googleusercontent.com/-kbD1OZrG7kI/UnmdupWzP1I/AAAAAAADFfg/g3SXEo1JXIs/s300-no/cQEBX.gif"}, 9 | "peak": {"image_url": "https://lh4.googleusercontent.com/-V9zKyTXHrD4/Unmdzpnb1UI/AAAAAAADFgc/-61luVxML_M/w500-h320-no/efuITnY.gif"}, 10 | "right": {"image_url": "https://lh6.googleusercontent.com/-vPkV8r6Mhy0/UnmdtC490bI/AAAAAAADFfM/Hil5QyuTX78/w500-h221-no/HuVXfMb.gif"}, 11 | "sup": {"image_url": "https://lh3.googleusercontent.com/-iqoKtA72rIo/Unmd7o17ZUI/AAAAAAADFhM/7-MLxTAXU7Q/w470-h248-no/yC5BsCS.gif"}, 12 | "nod": {"image_url": "https://lh4.googleusercontent.com/-byviKRrwH5c/Unny4gWkMqI/AAAAAAADFkc/OckIZqXDCY4/w500-h225-no/iPOS6.gif"}, 13 | "thumbsup": {"image_url": "https://lh4.googleusercontent.com/-1l8KOia5vis/Unny3Jw851I/AAAAAAADFkY/bRLzh5VyPbc/w312-h176-no/ZCGrZ.gif"}, 14 | "dance1": {"image_url": "https://lh5.googleusercontent.com/-5sN8WVo6Myc/Unny6ZsWZGI/AAAAAAADFk8/JssUbaDRG_M/w250-h303-no/mbjD3.gif"}, 15 | "dance2": {"image_url": "https://lh5.googleusercontent.com/-2aPDr_v7Jgc/Unny3pXSfvI/AAAAAAADFkI/KyloHLe5DaM/w400-h234-no/gary.gif"}, 16 | "dance3": {"image_url": "https://lh5.googleusercontent.com/-rqM2JLfd2xg/Unny2Pvpy1I/AAAAAAADFjY/Zx6D93XEws4/w480-h340-no/1tumblr_lgp6q5NhE21qcjtu8o1_500.gif"}, 17 | "sidepunch": {"image_url": "https://lh6.googleusercontent.com/-HZYoVvXWc_s/UnmfpjqTbiI/AAAAAAADFic/q3dGlwfxV2w/w500-h247-no/code-27.gif"}, 18 | "tyrionbird": {"image_url": "https://lh5.googleusercontent.com/-6frbpEP6M3g/Unmfl9i3fcI/AAAAAAADFh8/SOpKxwyOk3k/w300-h149-no/code-19.gif"}, 19 | "obiwan": {"image_url": "http://i.giphy.com/2vERRTu559XQk.gif"}, 20 | "wat": {"image_url": "http://i.giphy.com/qxtxlL4sFFle.gif"}, 21 | "ivan": {"image_url": "http://i.giphy.com/2OMHmoFMiJjfq.gif"}, 22 | "freakout": {"image_url": "http://i.giphy.com/FwpecpDvcu7vO.gif"}, 23 | "boom-chuck": {"image_url": "http://i.giphy.com/10PkQyXWagNrqg.gif"}, 24 | "boom-drake": {"image_url": "http://i.giphy.com/8vW915bv0DqXC.gif"}, 25 | "boom-dany": {"image_url": "http://i.giphy.com/LrN9NbJNp9SWQ.gif"}, 26 | "boom-moon": {"image_url": "http://i.giphy.com/ydMNTWYVjSEFi.gif"}, 27 | "boom-franco": {"image_url": "http://i.giphy.com/b3u8anVaWFQ9G.gif"}, 28 | "boom-starcraft": {"image_url": "http://i.giphy.com/5xtDarxQ05ee2ktRA5i.gif"}, 29 | "boom-anime": {"image_url": "http://i.giphy.com/fMNNDKtOVbxSM.gif"}, 30 | "boom-denzel":{"image_url":"http://i.imgur.com/1LXMDns.gif"}, 31 | "boom-ump":{"image_url":"http://cdn0.sbnation.com/imported_assets/1160455/umpboom.gif"}, 32 | "fun": {"image_url": "http://s-ak.buzzfed.com/static/imagebuzz/web04/2011/3/14/10/anigif_fun-fun-fun-29834-1300112565-20.gif"}, 33 | "fthis": {"image_url": "http://i.giphy.com/iICCUuGJOhOnK.gif"}, 34 | "disappear": {"image_url": "http://i.giphy.com/4pMX5rJ4PYAEM.gif"}, 35 | "cry": {"image_url": "http://i.giphy.com/10tIjpzIu8fe0.gif"}, 36 | "micdrop":{"image_url":"http://i.imgur.com/9XHl18c.gif"}, 37 | "david-s-pumpkins": {"image_url": "http://i.giphy.com/3oriNYMXEh2K5l4D9C.gif"} 38 | } 39 | -------------------------------------------------------------------------------- /uptime/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "sort" 8 | "time" 9 | 10 | "github.com/iron-io/iron_go/cache" 11 | ) 12 | 13 | var c *cache.Cache 14 | var client *PingdomClient 15 | var slackClient *slackC 16 | 17 | type Config struct { 18 | Username string `json:"username"` 19 | Password string `json:"password"` 20 | ApiKey string `json:"api_key"` 21 | WebhookUrl string `json:"webhook_url"` 22 | MqIds []int `json:"mq"` 23 | WorkerIds []int `json:"worker"` 24 | OtherIds []int `json:"other"` 25 | } 26 | 27 | func main() { 28 | c = cache.New("uptime_bot") 29 | 30 | b, err := ioutil.ReadFile("config.json") 31 | 32 | if err != nil { 33 | fmt.Println(err) 34 | return 35 | } 36 | 37 | cfg := &Config{} 38 | err = json.Unmarshal(b, cfg) 39 | if err != nil { 40 | fmt.Println(err) 41 | return 42 | } 43 | 44 | client = &PingdomClient{ 45 | Username: cfg.Username, 46 | Password: cfg.Password, 47 | ApiKey: cfg.ApiKey, 48 | } 49 | 50 | slackClient = &slackC{ 51 | url: cfg.WebhookUrl, 52 | } 53 | since := time.Now() 54 | mqUptimes := client.getUptimes(cfg.MqIds, since) 55 | workerUptimes := client.getUptimes(cfg.WorkerIds, since) 56 | otherUptimes := client.getUptimes(cfg.OtherIds, since) 57 | 58 | var attachments []Attachment 59 | attachments = append(attachments, buildReportsAttachment("IronMQ", mqUptimes)) 60 | attachments = append(attachments, buildReportsAttachment("IronWorker", workerUptimes)) 61 | attachments = append(attachments, buildReportsAttachment("Other", otherUptimes)) 62 | 63 | var reports UptimeReports 64 | reports = append(reports, mqUptimes...) 65 | reports = append(reports, workerUptimes...) 66 | reports = append(reports, otherUptimes...) 67 | sort.Sort(reports) 68 | 69 | if len(reports) > 2 { 70 | if reports[0].DayAgo.uptimePercentage() < 1 { 71 | attachments = append(attachments, buildReportAttachment(reports[0])) 72 | attachments = append(attachments, buildReportAttachment(reports[1])) 73 | } 74 | } 75 | // fmt.Println("", attachments) 76 | slackClient.post("", attachments) 77 | } 78 | 79 | func buildReportsAttachment(name string, u UptimeReports) Attachment { 80 | var attachment Attachment 81 | if u.dailyUptime() >= .9998 { 82 | attachment.Color = "#2DD700" 83 | } else { 84 | attachment.Color = "#BD2121" 85 | } 86 | attachment.Title = name 87 | attachment.Fields = append(attachment.Fields, Field{ 88 | Value: fmt.Sprintf("%.4f%% uptime over the last 24 hours (%s total downtime)", u.dailyUptime()*100, u.totalDailyDowntime()), 89 | Short: false, 90 | }) 91 | attachment.Fields = append(attachment.Fields, Field{ 92 | Title: "7 days", 93 | Value: fmt.Sprintf("%.4f%% (%s)", u.weeklyUptime()*100, u.totalWeeklyDowntime()), 94 | Short: true, 95 | }) 96 | attachment.Fields = append(attachment.Fields, Field{ 97 | Title: "1 month", 98 | Value: fmt.Sprintf("%.4f%% (%s)", u.monthlyUptime()*100, u.totalMonthlyDowntime()), 99 | Short: true, 100 | }) 101 | return attachment 102 | } 103 | 104 | // Build an attachment for a single report. 105 | func buildReportAttachment(u *UptimeReport) Attachment { 106 | var attachment Attachment 107 | attachment.Title = u.Name 108 | attachment.Color = "#C77838" 109 | attachment.Fields = append(attachment.Fields, Field{ 110 | Title: "24 hours", 111 | Value: fmt.Sprintf("%.4f%% (%s)", u.DayAgo.uptimePercentage()*100, 112 | (u.DayAgo.Downtime() * time.Second).String()), 113 | Short: true, 114 | }) 115 | attachment.Fields = append(attachment.Fields, Field{ 116 | Title: "7 Days", 117 | Value: fmt.Sprintf("%.4f%% (%s)", u.WeekAgo.uptimePercentage()*100, 118 | (u.WeekAgo.Downtime() * time.Second).String()), 119 | Short: true, 120 | }) 121 | 122 | return attachment 123 | } 124 | -------------------------------------------------------------------------------- /salesforce/google_datastore.rb: -------------------------------------------------------------------------------- 1 | # This is abstracted into it's own class so you could swap it out for any other datastore without having to change the bot. 2 | 3 | require 'google/api_client' 4 | 5 | class GoogleDatastore 6 | 7 | attr_accessor :config, :dataset_id, :datastore, :client 8 | 9 | def initialize(gconfig) 10 | @config = gconfig 11 | # Same as project_id 12 | @dataset_id = gconfig.project_id 13 | 14 | # Google::APIClient.logger.level = Logger::INFO 15 | gclient = Google::APIClient.new(:application_name => 'Slacker', :application_version => '1.0.0') 16 | @datastore = gclient.discovered_api('datastore', 'v1beta2') 17 | 18 | # Load our credentials for the service account 19 | key = Google::APIClient::KeyUtils.load_from_pkcs12("gkey.p12", "notasecret") 20 | 21 | # Set authorization scopes and credentials. 22 | gclient.authorization = Signet::OAuth2::Client.new( 23 | :token_credential_uri => 'https://accounts.google.com/o/oauth2/token', 24 | :audience => 'https://accounts.google.com/o/oauth2/token', 25 | :scope => ['https://www.googleapis.com/auth/datastore', 26 | 'https://www.googleapis.com/auth/userinfo.email'], 27 | :issuer => gconfig.service_account_id, 28 | :signing_key => key) 29 | 30 | # Authorize the client. 31 | puts "authorizing to google..." 32 | gclient.authorization.fetch_access_token! 33 | 34 | @client = gclient 35 | 36 | end 37 | 38 | def lckey 39 | return {:path => [{:kind => 'CheckDates', :name => 'last_check'}]} # this name should be unique per user 40 | end 41 | 42 | # Returns the last date we checked Salesforce 43 | def get_last_check 44 | 45 | resp = client.execute( 46 | :api_method => datastore.datasets.lookup, 47 | :parameters => {:datasetId => dataset_id}, 48 | :body_object => { 49 | # Set the transaction, so we get a consistent snapshot of the 50 | # value at the time the transaction started. 51 | # :readOptions => {:transaction => tx}, 52 | # Add one entity key to the lookup request, with only one 53 | # :path element (i.e. no parent) 54 | :keys => [lckey] 55 | }) 56 | if !resp.data.found.empty? 57 | # Get the entity from the response if found. 58 | entity = resp.data.found[0].entity 59 | puts "Found last_check entity: #{entity.inspect}" 60 | # Get `question` property value. 61 | last_check = entity.properties.last_check.dateTimeValue.to_datetime 62 | else 63 | last_check = Date.today.prev_day.to_datetime # "2015-01-01T00:00:01z" 64 | end 65 | puts "last_check: #{last_check.inspect} class=#{last_check.class.name}" 66 | return last_check 67 | end 68 | 69 | def insert_new_check_date(t=Time.now) 70 | # If the entity is not found create it. 71 | entity = { 72 | # Set the entity key with only one `path` element: no parent. 73 | :key => lckey, 74 | # Set the entity properties: 75 | # - a utf-8 string: `question` 76 | # - a 64bit integer: `answer` 77 | :properties => { 78 | :last_check => {:dateTimeValue => t.to_datetime.rfc3339()}, 79 | } 80 | } 81 | # Build a mutation to insert the new entity. 82 | mutation = {:upsert => [entity]} 83 | 84 | # Commit the transaction and the insert mutation if the entity was not found. 85 | resp = client.execute( 86 | :api_method => datastore.datasets.commit, 87 | :parameters => {:datasetId => dataset_id}, 88 | :body_object => { 89 | # :transaction => tx, 90 | # todo: I couldn't get transactions to work?!?! get this: \"reason\": \"INVALID_ARGUMENT\",\n \"message\": \"unknown transaction handle\"\n 91 | :mode => 'NON_TRANSACTIONAL', 92 | :mutation => mutation 93 | }) 94 | puts "body=#{resp.body.inspect}" 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /aws_cost_bot/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.1) 5 | addressable (2.3.8) 6 | amazon-pricing (0.1.63) 7 | mechanize (~> 2.7.2) 8 | builder (3.2.2) 9 | domain_name (0.5.24) 10 | unf (>= 0.0.5, < 1.0.0) 11 | eventmachine (1.0.7) 12 | excon (0.45.4) 13 | faraday (0.9.1) 14 | multipart-post (>= 1.2, < 3) 15 | faraday_middleware (0.10.0) 16 | faraday (>= 0.7.4, < 0.10) 17 | faye-websocket (0.9.2) 18 | eventmachine (>= 0.12.0) 19 | websocket-driver (>= 0.5.1) 20 | fission (0.5.0) 21 | CFPropertyList (~> 2.2) 22 | fog (1.32.0) 23 | fog-atmos 24 | fog-aws (>= 0.6.0) 25 | fog-brightbox (~> 0.4) 26 | fog-core (~> 1.32) 27 | fog-ecloud (= 0.1.1) 28 | fog-google (>= 0.0.2) 29 | fog-json 30 | fog-local 31 | fog-powerdns (>= 0.1.1) 32 | fog-profitbricks 33 | fog-radosgw (>= 0.0.2) 34 | fog-riakcs 35 | fog-sakuracloud (>= 0.0.4) 36 | fog-serverlove 37 | fog-softlayer 38 | fog-storm_on_demand 39 | fog-terremark 40 | fog-vmfusion 41 | fog-voxel 42 | fog-xml (~> 0.1.1) 43 | ipaddress (~> 0.5) 44 | nokogiri (~> 1.5, >= 1.5.11) 45 | fog-atmos (0.1.0) 46 | fog-core 47 | fog-xml 48 | fog-aws (0.7.3) 49 | fog-core (~> 1.27) 50 | fog-json (~> 1.0) 51 | fog-xml (~> 0.1) 52 | ipaddress (~> 0.8) 53 | fog-brightbox (0.8.0) 54 | fog-core (~> 1.22) 55 | fog-json 56 | inflecto (~> 0.0.2) 57 | fog-core (1.32.0) 58 | builder 59 | excon (~> 0.45) 60 | formatador (~> 0.2) 61 | mime-types 62 | net-scp (~> 1.1) 63 | net-ssh (>= 2.1.3) 64 | fog-ecloud (0.1.1) 65 | fog-core 66 | fog-xml 67 | fog-google (0.0.7) 68 | fog-core 69 | fog-json 70 | fog-xml 71 | fog-json (1.0.2) 72 | fog-core (~> 1.0) 73 | multi_json (~> 1.10) 74 | fog-local (0.2.1) 75 | fog-core (~> 1.27) 76 | fog-powerdns (0.1.1) 77 | fog-core (~> 1.27) 78 | fog-json (~> 1.0) 79 | fog-xml (~> 0.1) 80 | fog-profitbricks (0.0.3) 81 | fog-core 82 | fog-xml 83 | nokogiri 84 | fog-radosgw (0.0.4) 85 | fog-core (>= 1.21.0) 86 | fog-json 87 | fog-xml (>= 0.0.1) 88 | fog-riakcs (0.1.0) 89 | fog-core 90 | fog-json 91 | fog-xml 92 | fog-sakuracloud (1.0.1) 93 | fog-core 94 | fog-json 95 | fog-serverlove (0.1.2) 96 | fog-core 97 | fog-json 98 | fog-softlayer (0.4.7) 99 | fog-core 100 | fog-json 101 | fog-storm_on_demand (0.1.1) 102 | fog-core 103 | fog-json 104 | fog-terremark (0.1.0) 105 | fog-core 106 | fog-xml 107 | fog-vmfusion (0.1.0) 108 | fission 109 | fog-core 110 | fog-voxel (0.1.0) 111 | fog-core 112 | fog-xml 113 | fog-xml (0.1.2) 114 | fog-core 115 | nokogiri (~> 1.5, >= 1.5.11) 116 | formatador (0.2.5) 117 | http-cookie (1.0.2) 118 | domain_name (~> 0.5) 119 | inflecto (0.0.2) 120 | ipaddress (0.8.0) 121 | iron_cache (1.4.2) 122 | iron_core (>= 0.5.1) 123 | iron_core (1.0.7) 124 | rest (>= 3.0.4) 125 | iron_worker (3.1.0) 126 | iron_core (>= 0.5.1) 127 | rest (>= 3.0.6) 128 | keen (0.9.2) 129 | addressable (~> 2.3.5) 130 | multi_json (~> 1.3) 131 | mechanize (2.7.3) 132 | domain_name (~> 0.5, >= 0.5.1) 133 | http-cookie (~> 1.0) 134 | mime-types (~> 2.0) 135 | net-http-digest_auth (~> 1.1, >= 1.1.1) 136 | net-http-persistent (~> 2.5, >= 2.5.2) 137 | nokogiri (~> 1.4) 138 | ntlm-http (~> 0.1, >= 0.1.1) 139 | webrobots (>= 0.0.9, < 0.2) 140 | mime-types (2.6.1) 141 | mini_portile (0.6.2) 142 | multi_json (1.11.2) 143 | multipart-post (2.0.0) 144 | net-http-digest_auth (1.4) 145 | net-http-persistent (2.9.4) 146 | net-scp (1.2.1) 147 | net-ssh (>= 2.6.5) 148 | net-ssh (2.9.2) 149 | netrc (0.10.3) 150 | nokogiri (1.6.6.2) 151 | mini_portile (~> 0.6.0) 152 | ntlm-http (0.1.1) 153 | rest (3.0.6) 154 | net-http-persistent (>= 2.9.1) 155 | netrc 156 | slack-api (1.1.6) 157 | faraday (>= 0.7, < 0.10) 158 | faraday_middleware (~> 0.8) 159 | faye-websocket (~> 0.9.2) 160 | multi_json (~> 1.0, >= 1.0.3) 161 | slack-notifier (1.2.1) 162 | slack_webhooks (0.0.10) 163 | slack-notifier (>= 0.5.1) 164 | unf (0.1.4) 165 | unf_ext 166 | unf_ext (0.0.7.1) 167 | webrobots (0.1.1) 168 | websocket-driver (0.6.2) 169 | websocket-extensions (>= 0.1.0) 170 | websocket-extensions (0.1.2) 171 | 172 | PLATFORMS 173 | ruby 174 | 175 | DEPENDENCIES 176 | amazon-pricing 177 | fog 178 | iron_cache 179 | iron_worker 180 | keen 181 | slack-api 182 | slack_webhooks 183 | -------------------------------------------------------------------------------- /salesforce/opportunity_bot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'iron_worker' 3 | require 'slack_webhooks' 4 | require 'restforce' 5 | require 'iron_cache' 6 | 7 | # Config models 8 | class SlackC 9 | attr_accessor :channel, :webhook_url 10 | end 11 | class SalesforceC 12 | attr_accessor :username, 13 | :password, 14 | :security_token, 15 | :client_id, 16 | :client_secret 17 | 18 | def set(h) 19 | h.each { |k, v| send("#{k}=", v) } 20 | end 21 | 22 | def to_hash 23 | # makes hash and symbolizes keys 24 | hash = {} 25 | instance_variables.each { |var| hash[var.to_s.delete("@").to_sym] = instance_variable_get(var) } 26 | hash 27 | end 28 | end 29 | 30 | class MyConfig 31 | attr_accessor :slack, :google, :salesforce 32 | 33 | def initialize 34 | @slack = SlackC.new 35 | @salesforce = SalesforceC.new 36 | end 37 | end 38 | 39 | config = MyConfig.new 40 | config.salesforce.set(IronWorker.config['salesforce']) 41 | 42 | config.slack.webhook_url = IronWorker.config['webhook_url'] 43 | config.slack.channel = IronWorker.config['channel'] 44 | 45 | @ic = IronCache::Client.new 46 | @cache = @ic.cache("salesbot") 47 | @last_check_key = "opportunity_bot_last_check" 48 | 49 | def get_last_check 50 | item = @cache.get(@last_check_key) 51 | if item != nil 52 | last_check = DateTime.parse(item.value) 53 | else 54 | last_check = Date.today.prev_day.to_datetime # "2015-01-01T00:00:01z" 55 | end 56 | puts "last_check: #{last_check.inspect} class=#{last_check.class.name}" 57 | return last_check 58 | end 59 | 60 | def insert_check_date 61 | @cache.put(@last_check_key, DateTime.now.rfc3339, :expires_in => 86400 * 7) 62 | end 63 | 64 | def get_opp_updates(config) 65 | 66 | client = Restforce.new(config.salesforce.to_hash) 67 | 68 | last_check = get_last_check 69 | # for testing: 70 | # last_check = Date.today.prev_day.to_datetime # "2015-01-01T00:00:01z" 71 | 72 | # Opportunity: https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_opportunity.htm 73 | query = "Select op.Id, op.Name, op.Amount, op.StageName, op.Probability, op.LastModifiedDate, op.CloseDate, op.AccountId, op.OwnerId from Opportunity op " + 74 | "where op.LastModifiedDate > #{last_check.to_datetime.rfc3339}" 75 | puts "query=#{query}" 76 | ops = client.query(query) 77 | posted = 0 78 | ops.each_with_index do |op, i| 79 | break if posted >= 3 80 | puts "op" 81 | p op 82 | puts "Op: id=#{op.Id} opname=#{op.Name} amount=#{op.Amount} probability=#{op.Probability}" 83 | p client.url(op) 84 | 85 | # Get account for the opp too 86 | query = "Select Id, Name, LastModifiedDate from Account where Id = '#{op.AccountId}'" 87 | puts "account query=#{query}" 88 | accounts = client.query(query) 89 | p accounts 90 | account = accounts.first 91 | 92 | # And owner 93 | query = "Select Id, Name, LastModifiedDate from User where Id = '#{op.OwnerId}'" 94 | puts "owner query=#{query}" 95 | owners = client.query(query) 96 | p owners 97 | owner = owners.first 98 | 99 | # OpportunityHistory: https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_opportunityhistory.htm 100 | history = client.query("select ID, StageName, Probability, Amount, ExpectedRevenue, CloseDate from OpportunityHistory where OpportunityId = '#{op.Id}' and CreatedDate > #{last_check.to_datetime.rfc3339}") 101 | history.each_with_index do |h, j| 102 | break if j >= 1 # only do one max 103 | puts "history" 104 | p h 105 | puts "History: id=#{h.Id} amount=#{h.Amount} stage=#{h.StageName} probability=#{h.Probability} closedate=#{h.CloseDate}" 106 | post_to_slack(config, client, op, account, owner, h) 107 | posted += 1 108 | end 109 | end 110 | 111 | insert_check_date() 112 | puts "Posted #{posted} updates to slack. " 113 | 114 | end 115 | 116 | def post_to_slack(config, sclient, op, account, owner, h) 117 | if h.Amount.nil? 118 | puts "h.Amount is nil" 119 | return 120 | end 121 | 122 | # todo: ADD OWNER -author_name ? 123 | s = "Opportunity for <#{sclient.url(account)}|#{account.Name}> updated." 124 | attachment = { 125 | "fallback" => s, 126 | "pretext" => s, 127 | "title" => "#{op.Name}", 128 | "title_link" => sclient.url(op), 129 | # "text" => "Stage: #{h.StageName}", 130 | # "image_url" => "http://caldwelljournal.com/wp-content/uploads/2015/01/Boom.jpg", 131 | # "color": "#764FA5" 132 | "fields" => [ 133 | { 134 | "title" => "Stage", 135 | "value" => "#{h.StageName}", 136 | "short" => true 137 | }, 138 | { 139 | "title" => "Amount", 140 | "value" => "$#{'%.2f' % h.Amount}", 141 | "short" => true 142 | }, 143 | { 144 | "title" => "Close Date", 145 | "value" => "#{op.CloseDate}", 146 | "short" => true 147 | }, 148 | # { 149 | # "title" => "Probability", 150 | # "value" => "#{'%.0f' % h.Probability}%", 151 | # "short" => true 152 | # }, 153 | # { 154 | # "title" => "Expected Revenue", 155 | # "value" => "$#{'%.2f' % h.ExpectedRevenue}", 156 | # "short" => true 157 | # }, 158 | { 159 | "title" => "Owner", 160 | "value" => "#{owner.Name}", 161 | "short" => true 162 | }, 163 | ] 164 | } 165 | # if op.StageName.include? "11" 166 | # # there's also 'warning' 167 | # attachment["color"] = 'danger' 168 | # elsif op.StageName.include?("06") || op.StageName.include?("08") || op.StageName.include?("10") 169 | # attachment["color"] = 'good' 170 | # end 171 | 172 | if op.Probability >= 70 173 | # there's also 'warning' 174 | attachment["color"] = 'good' 175 | elsif op.Probability <= 20 176 | attachment["color"] = 'danger' 177 | elsif op.Probability <= 40 178 | attachment["color"] = 'warning' 179 | end 180 | if op.Probability == 100 181 | attachment["image_url"] = "http://caldwelljournal.com/wp-content/uploads/2015/01/Boom.jpg" 182 | end 183 | 184 | puts "posting #{attachment.inspect} to slack..." 185 | 186 | # uncomment line below for testing 187 | # sh.channel = sh.username 188 | 189 | # puts "Posting #{text} to #{channel}..." 190 | notifier = Slack::Notifier.new config.slack.webhook_url 191 | notifier.channel = "#{config.slack.channel}" if config.slack.channel 192 | notifier.username = 'salesbot' 193 | resp = notifier.ping "", attachments: [attachment] 194 | p resp 195 | p resp.message 196 | puts "done" 197 | end 198 | 199 | get_opp_updates(config) 200 | 201 | -------------------------------------------------------------------------------- /uptime/pingdom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strconv" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const host = "https://api.pingdom.com/api/2.0/" 14 | 15 | type PingdomClient struct { 16 | Username string 17 | Password string 18 | ApiKey string 19 | client http.Client 20 | } 21 | 22 | type Check struct { 23 | Id int `json:"id"` 24 | Name string `json:"name"` 25 | Status string `json:"status"` 26 | } 27 | 28 | func (c *PingdomClient) Get(endpoint string) (*http.Response, error) { 29 | req, err := http.NewRequest("GET", host+endpoint, nil) 30 | if err != nil { 31 | return nil, err 32 | } 33 | req.SetBasicAuth(c.Username, c.Password) 34 | req.Header.Set("App-Key", c.ApiKey) 35 | res, err := c.client.Do(req) 36 | return res, err 37 | } 38 | 39 | func (c *PingdomClient) getChecks() ([]Check, error) { 40 | res, err := c.Get("checks/") 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | b, err := ioutil.ReadAll(res.Body) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | checkList := struct { 51 | Checks []Check `json:"checks"` 52 | }{} 53 | err = json.Unmarshal(b, &checkList) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return checkList.Checks, err 58 | } 59 | 60 | func (c *PingdomClient) getCheckById(id int) (Check, error) { 61 | endpoint := fmt.Sprintf("checks/%d", id) 62 | res, err := c.Get(endpoint) 63 | if err != nil || res.StatusCode != 200 { 64 | return Check{}, err 65 | } 66 | 67 | b, err := ioutil.ReadAll(res.Body) 68 | if err != nil { 69 | return Check{}, err 70 | } 71 | 72 | check := struct { 73 | Check Check `json:"check"` 74 | }{} 75 | err = json.Unmarshal(b, &check) 76 | if err != nil { 77 | return Check{}, err 78 | } 79 | return check.Check, err 80 | } 81 | 82 | func (c *PingdomClient) getUptime(check *Check, from time.Time) (*Status, error) { 83 | fromString := strconv.FormatInt(from.Unix(), 10) 84 | endpoint := fmt.Sprintf("summary.average/%d?includeuptime=true&from=%s", check.Id, fromString) 85 | res, err := c.Get(endpoint) 86 | if err != nil || res.StatusCode != 200 { 87 | return &Status{}, err 88 | } 89 | 90 | b, err := ioutil.ReadAll(res.Body) 91 | if err != nil { 92 | return &Status{}, err 93 | } 94 | s := struct { 95 | S struct { 96 | Status Status `json:"status"` 97 | } `json:"summary"` 98 | }{} 99 | err = json.Unmarshal(b, &s) 100 | if err != nil { 101 | fmt.Println(err) 102 | } 103 | return &s.S.Status, err 104 | } 105 | 106 | func (c *PingdomClient) getUptimes(alertIds []int, since time.Time) UptimeReports { 107 | var checks []*Check 108 | for _, id := range alertIds { 109 | check, err := c.getCheckById(id) 110 | if err != nil { 111 | fmt.Println(err) 112 | } else { 113 | checks = append(checks, &check) 114 | } 115 | } 116 | 117 | var uptimeReports UptimeReports 118 | for _, check := range checks { 119 | // We don't want anything thats paused 120 | if check.Status != "paused" && check.Status != "unknown" { 121 | u := &UptimeReport{ 122 | Name: check.Name, 123 | Id: check.Id, 124 | } 125 | if ur, err := client.getUptime(check, since.AddDate(0, 0, -1)); err == nil { 126 | u.DayAgo = ur 127 | } 128 | if ur, err := client.getUptime(check, since.AddDate(0, 0, -7)); err == nil { 129 | u.WeekAgo = ur 130 | } 131 | if ur, err := client.getUptime(check, since.AddDate(0, -1, 0)); err == nil { 132 | u.MonthAgo = ur 133 | } 134 | uptimeReports = append(uptimeReports, u) 135 | } 136 | } 137 | return uptimeReports 138 | } 139 | 140 | type UptimeReport struct { 141 | Id int `json:"id"` 142 | Name string `json:"name"` 143 | DayAgo *Status 144 | WeekAgo *Status 145 | MonthAgo *Status 146 | } 147 | 148 | type Status struct { 149 | Totalup int64 `json:"totalup"` 150 | Totaldown int64 `json:"totaldown"` 151 | Totalunknown int64 `json:"totalunknown"` 152 | } 153 | 154 | type UptimeReports []*UptimeReport 155 | 156 | func (u *Status) uptimePercentage() float64 { 157 | if u.Totalup+u.Totaldown == 0 { 158 | return 1.0 159 | } 160 | return float64(u.Totalup) / (float64(u.Totaldown) + float64(u.Totalup)) 161 | } 162 | func (u *Status) save(key string) { 163 | percentage := strconv.FormatFloat(u.uptimePercentage(), 'f', 8, 64) 164 | c.Set(key, percentage) 165 | } 166 | 167 | // Change in downtime percentage 168 | func (u *Status) delta(key string) float64 { 169 | value, err := c.Get(key) 170 | if err != nil { 171 | return 0.0 172 | } 173 | old, err := strconv.ParseFloat(value.(string), 64) 174 | if err != nil { 175 | return 0.0 176 | } 177 | // If our uptime is up, we want to show a positive change instead of negative 178 | return (u.uptimePercentage() - old) / old 179 | } 180 | func (u *Status) Downtime() time.Duration { 181 | return time.Duration(u.Totaldown) 182 | } 183 | 184 | func (u *UptimeReport) save() { 185 | // key-format is id-(days_ago) 186 | id := strconv.Itoa(u.Id) 187 | u.DayAgo.save(id + "1") 188 | u.WeekAgo.save(id + "7") 189 | u.MonthAgo.save(id + "30") 190 | } 191 | 192 | func (u *UptimeReport) deltas() []float64 { 193 | id := strconv.Itoa(u.Id) 194 | return []float64{ 195 | u.DayAgo.delta(id + "1"), 196 | u.WeekAgo.delta(id + "7"), 197 | u.MonthAgo.delta(id + "30"), 198 | } 199 | } 200 | 201 | func (ur UptimeReports) dailyUptime() float64 { 202 | var up int64 203 | var down int64 204 | for _, u := range ur { 205 | up += u.DayAgo.Totalup 206 | down += u.DayAgo.Totaldown 207 | } 208 | return float64(up) / float64(up+down) 209 | } 210 | 211 | func (ur UptimeReports) totalDailyDowntime() string { 212 | var sum time.Duration 213 | for _, u := range ur { 214 | sum += time.Duration(u.DayAgo.Totaldown) 215 | } 216 | sum = sum * time.Second 217 | return sum.String() 218 | } 219 | 220 | func (ur UptimeReports) weeklyUptime() float64 { 221 | var up int64 222 | var down int64 223 | for _, u := range ur { 224 | up += u.WeekAgo.Totalup 225 | down += u.WeekAgo.Totaldown 226 | } 227 | return float64(up) / float64(up+down) 228 | } 229 | 230 | func (ur UptimeReports) totalWeeklyDowntime() string { 231 | var sum time.Duration 232 | for _, u := range ur { 233 | sum += time.Duration(u.WeekAgo.Totaldown) 234 | } 235 | sum = sum * time.Second 236 | return sum.String() 237 | } 238 | 239 | func (ur UptimeReports) monthlyUptime() float64 { 240 | var up int64 241 | var down int64 242 | for _, u := range ur { 243 | up += u.MonthAgo.Totalup 244 | down += u.MonthAgo.Totaldown 245 | } 246 | return float64(up) / float64(up+down) 247 | } 248 | func (ur UptimeReports) totalMonthlyDowntime() string { 249 | var sum time.Duration 250 | for _, u := range ur { 251 | sum += time.Duration(u.MonthAgo.Totaldown) 252 | } 253 | sum = sum * time.Second 254 | return sum.String() 255 | } 256 | 257 | func (ur UptimeReports) save() { 258 | var wg sync.WaitGroup 259 | wg.Add(len(ur)) 260 | for _, u := range ur { 261 | go func(u *UptimeReport) { 262 | defer wg.Done() 263 | u.save() 264 | }(u) 265 | } 266 | wg.Wait() 267 | } 268 | 269 | // sort in ascending order 270 | func (ur UptimeReports) Len() int { return len(ur) } 271 | func (ur UptimeReports) Swap(i, j int) { ur[i], ur[j] = ur[j], ur[i] } 272 | func (ur UptimeReports) Less(i, j int) bool { 273 | return ur[i].DayAgo.uptimePercentage() < ur[j].DayAgo.uptimePercentage() 274 | } 275 | -------------------------------------------------------------------------------- /aws_cost_bot/costbot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bundle/bundler/setup' 2 | require 'iron_worker' 3 | require 'slack_webhooks' 4 | require 'iron_cache' 5 | require 'fog' 6 | require 'amazon-pricing' 7 | require 'slack' 8 | 9 | slack = Slack::Client.new(:token => IronWorker.config['slack_token']) 10 | channel = IronWorker.config['channel'] 11 | channel_id = "x" 12 | 13 | # apparently the only way to get the channel id is like this: 14 | slack.channels_list['channels'].each do |c| 15 | # puts "name: #{c['name']}, id: #{c['id']}" 16 | if c['name'] == channel || c['name'] == channel[1..channel.length] 17 | channel_id = c['id'] 18 | break 19 | end 20 | end 21 | 22 | # todo: since this one uses the api directly already, don't bother with incoming webhook 23 | sh = SlackWebhooks::Hook.new('costbot', IronWorker.payload, IronWorker.config['webhook_url']) 24 | sh.icon_url = "http://images.clipartpanda.com/save-money-icon-save-money-icon-iebaaazn.png" 25 | sh.channel = channel 26 | 27 | @ic = IronCache::Client.new 28 | @cache = @ic.cache("costbot") 29 | 30 | compute = Fog::Compute.new( 31 | :provider => :aws, 32 | :aws_secret_access_key => IronWorker.config['aws']['secret_key'], 33 | :aws_access_key_id => IronWorker.config['aws']['access_key'] 34 | ) 35 | 36 | reserved_hash = {} 37 | reserved = compute.describe_reserved_instances 38 | reserved.body['reservedInstancesSet'].each do |ris| 39 | next if ris['state'] != 'active' 40 | # p ris 41 | # todo: use offeringType, maybe use amount too? amount is hourly cost, fixed price is up front cost 42 | az = ris['availabilityZone'] 43 | itype = ris['instanceType'] 44 | azhash = reserved_hash[az] || {} 45 | itypehash = azhash[itype] || {} 46 | itypehash['count'] = (itypehash['count']||0) + ris['instanceCount'] 47 | azhash[itype] = itypehash 48 | reserved_hash[az] = azhash 49 | end 50 | 51 | # Retrieve pricing 52 | price_list = AwsPricing::Ec2PriceList.new 53 | 54 | byzone = {} 55 | projects = {} 56 | 57 | puts "Region,Availability Zone,Instance Id,Instance IP,Instance Type,On-Demand Price Per Month" 58 | compute.servers.each do |server| 59 | # p server 60 | region = server.availability_zone[0...server.availability_zone.length-1] 61 | # p region 62 | # r2 = price_list.get_region(region) 63 | # p r2.ec2_instance_types[0].name 64 | # exit 65 | az = server.availability_zone 66 | itype = server.flavor_id 67 | 68 | instance_type = price_list.get_instance_type(region, server.flavor_id) 69 | price_per_hour = instance_type.price_per_hour(:linux, :ondemand) 70 | price_per_month = price_per_hour*24*30.4 71 | project_name = server.tags['Name'] 72 | 73 | project_hash = projects[project_name] || {} 74 | project_hash['price_per_month'] = (project_hash['price_per_month'] || 0.0) + price_per_month 75 | project_hash['count'] = (project_hash['count'] || 0) + 1 76 | 77 | azs = project_hash['azs'] || {} 78 | azhash = azs[az] || {} 79 | itypehash = azhash[itype] || {} 80 | itypehash['count'] = (itypehash['count']||0) + 1 81 | azhash[itype] = itypehash 82 | azs[az] = azhash 83 | project_hash['azs'] = azs 84 | 85 | projects[project_name] = project_hash 86 | 87 | # also aggregate by zone/type 88 | azhash = byzone[az] || {} 89 | itypehash = azhash[itype] || {} 90 | itypehash['count'] = (itypehash['count']||0) + 1 91 | azhash[itype] = itypehash 92 | byzone[az] = azhash 93 | 94 | # puts "#{project_name}, #{region},#{az},#{server.id},#{server.public_ip_address},#{itype},$#{price_per_hour},$#{price_per_month}" 95 | end 96 | 97 | 98 | def write_table(filename, table) 99 | File.open(filename, 'w') do |file| 100 | table.each do |row| 101 | row.each do |c| 102 | file.write(c) 103 | file.write(',') 104 | end 105 | file.write("\n") 106 | end 107 | end 108 | end 109 | 110 | def stringify_table(table, separator=",") 111 | s = "" 112 | table.each do |row| 113 | row.each do |c| 114 | s << c << separator 115 | end 116 | s << "\n" 117 | end 118 | s 119 | end 120 | 121 | # Sort project costs by cost desc 122 | sorted_projects = projects.sort_by { |k,v| v['price_per_month'] }.reverse 123 | 124 | total_cost = 0.0 125 | projects_costs_table = [["Project", "Servers", "Monthly Cost"]] 126 | sorted_projects.each_with_index do |a,i| 127 | k = a[0] 128 | v = a[1] 129 | total_cost += v['price_per_month'] 130 | puts "#{k}, count: #{v['count']}, price_per_month: #{sprintf('$%.2f', v['price_per_month'])}" 131 | # p v['azs'] 132 | projects_costs_table << ["#{k}", "#{v['count']}", "#{sprintf('$%.2f', v['price_per_month'])}"] 133 | end 134 | 135 | write_table('costs.csv', projects_costs_table) 136 | 137 | # Now for RI coverage 138 | total_servers = 0 139 | total_covered = 0 140 | extra_ris = 0 141 | ri_table = [["Zone","Type","Count","RI's","NOT Covered"]] 142 | File.open('ri-coverage.csv', 'w') do |file| 143 | byzone.each_pair do |zone,v| 144 | ris = reserved_hash[zone] || {} 145 | v.each_pair do |itype,itv| 146 | # Now compare v to ris 147 | rit = ris[itype] || {} 148 | icount = itv['count'] 149 | ricount = rit['count'] || 0 150 | uncovered = icount - ricount 151 | ri_table << ["#{zone}","#{itype}","#{icount}","#{ricount}","#{uncovered}"] 152 | total_servers += icount 153 | if uncovered < 0 154 | extra_ris += -uncovered 155 | total_covered += icount 156 | else 157 | total_covered += ricount 158 | end 159 | end 160 | end 161 | end 162 | write_table('ri-coverage.csv', ri_table) 163 | 164 | 165 | # todo: Store yesterdays data then compare to show differences 166 | # expires_in = 86400 167 | # users_vote_key = "#{votename}-user:#{sh.username}" 168 | # item = @cache.get(users_vote_key) 169 | # if item 170 | # end 171 | # @cache.put(users_vote_key, split[1], :expires_in => expires_in) 172 | 173 | attachments = [] 174 | 175 | percent_covered = 1.0 * total_covered / total_servers * 100.0 176 | p percent_covered 177 | if percent_covered < 75.0 178 | text = "Coverage is Bad!" 179 | color = "warning" 180 | else 181 | text = "Coverage is OK" 182 | color = "#00CC66" 183 | end 184 | fallback = text 185 | attachment = { 186 | "fallback" => fallback, 187 | "text" => text, 188 | "color" => color, 189 | "mrkdwn_in" => ["text", "pretext"], 190 | "fields" => [ 191 | { 192 | "title" => "Servers", 193 | "value" => "#{total_servers}", 194 | "short" => true 195 | }, 196 | { 197 | "title" => "Covered", 198 | "value" => "#{total_covered}", 199 | "short" => true 200 | }, 201 | { 202 | "title" => "Est. Monthly Cost", 203 | "value" => "#{sprintf('$%.2f', total_cost)}", 204 | "short" => true 205 | }, 206 | { 207 | "title" => "Percent Covered", 208 | "value" => "#{sprintf('%.0f', percent_covered)}%", 209 | "short" => true 210 | }, 211 | { 212 | "title" => "Unused Ri's", 213 | "value" => "#{extra_ris}", 214 | "short" => true 215 | }, 216 | 217 | ] 218 | } 219 | attachments << attachment 220 | 221 | channels = "#{channel_id}" # comma separated list 222 | content = stringify_table(projects_costs_table, "\t") 223 | p content 224 | p slack.files_upload(content: content, title: "Costs by Project.md", channels: channels) 225 | content = stringify_table(ri_table, "\t") 226 | p content 227 | p slack.files_upload(content: content, title: "RI Coverage.md", channels: channels) 228 | 229 | # my_file = Faraday::UploadIO.new("x.html", 'text/html') 230 | # p slack.files_upload(file: my_file, filetype: 'text/html') 231 | 232 | sh.send("This is your daily server report.", attachments: attachments) 233 | --------------------------------------------------------------------------------