Add commando.io service #945

Merged
merged 2 commits into from Oct 25, 2014
View
@@ -0,0 +1,14 @@
+[Commando.io](https://commando.io) is a simpler way to manage servers online.
+
+Commando.io makes it simple to execute commands on groups of servers from a beautiful web interface.
+
+Install Notes
+-------------
+
+ * **Account alias** - Your account alias is simply the subdomain that you access Commando.io with. For example the account alias of `https://foo.commando.io` is `foo`.
+ * **API token secret key** - A valid API token secret key. You may create API tokens on the settings page in the Commando.io web interface.
+ * **Recipe** - The recipe you wish to execute. You may find recipe ids in the in the Commando.io web interface.
+ * **Server** - A single server id. You may find server ids in the modal popup when clicking a server in the Commando.io web interface.
+ * **Groups** - A list of group ids seperated by commas. You may find group ids in the modal popup when clicking a group in the Commando.io web interface.
+ * **Halt on stderr** _(Optional)_ - If a server returns stderr during execution, halt and prevent the remaining servers from executing the recipe.
+ * **Notes** _(Optional)_ - Notes and comments you wish to attach to this execution. Markdown is supported.
@@ -0,0 +1,104 @@
+# Commando.io GitHub services integration
+class Service::Commandoio < Service::HttpPost
+
+ # Hook information
+ self.title = 'Commando.io'
+ self.hook_name = 'commandoio'
+
+ # Support and maintainer information
+ url 'https://commando.io'
+ logo_url 'https://static.commando.io/img/favicon-250.png'
+
+ supported_by :web => 'https://commando.io',
+ :twitter => '@commando_io',
+ :email => 'hello@commando.io'
+
+ maintained_by :web => 'https://unscramble.co.jp',
+ :github => 'aw',
+ :twitter => '@alexandermensa'
+
+ # Form fields
+ password :api_token_secret_key
+
+ string :account_alias,
+ :recipe,
+ :server,
+ :groups,
+ :notes
+
+ boolean :halt_on_stderr
+
+ # Only include these in the debug logs
+ white_list :account_alias,
+ :recipe,
+ :server,
+ :groups,
+ :halt_on_stderr
+
+ def receive_event
+ validate_config
+ validate_server_groups
+
+ url = "recipes/#{data['recipe']}/execute"
+
+ http.basic_auth data['account_alias'], data['api_token_secret_key']
+
+ http.ssl[:verify] = true
+ http.url_prefix = "https://api.commando.io/v1/"
+
+ groups = data['groups'].split(',').map {|x| x.strip } if data['groups']
+
+ params = { :payload => generate_json(payload) }
+
+ params.merge!(:server => data['server']) if data['server']
+ params.merge!(:groups => groups) unless groups.nil?
+ params.merge!(:halt_on_stderr => data['halt_on_stderr']) if data['halt_on_stderr']
+ params.merge!(:notes => CGI.escape(data['notes'])) if data['notes']
+
+ http_post url, params
+ end
+
+ # Validates the required config values
+ #
+ # Raises an error if a config value is invalid or empty
+ def validate_config
+ %w(api_token_secret_key account_alias recipe server groups).each {|key|
+ raise_config_error("Invalid or empty #{key}") if is_error? key, config_value(key)
+ }
+ end
+
+ # Validates the server and groups config values
+ #
+ # Raises an error if one or the other is missing, or if both are set (XOR)
+ def validate_server_groups
+ # XOR the server and groups
+ raise_config_error("Server or Groups must be set, but not both.") unless config_value('server').empty? ^ config_value('groups').empty?
+ end
+
+ # Check if there's an error in the provided value
+ #
+ # Returns a boolean
+ def is_error?(key, value)
+ case key
+ when 'api_token_secret_key' then true if value.empty? ||
+ value !~ /\Askey_[a-zA-Z0-9]+\z/
+
+ when 'account_alias' then true if value.empty? ||
+ value.length > 15 ||
+ value !~ /\A[a-z0-9]+\z/
+
+ when 'recipe' then true if value.empty? ||
+ value.length > 25 ||
+ value !~ /\A[a-zA-Z0-9_]+\z/
+
+ when 'server' then true if !value.empty? &&
+ value !~ /\A[a-zA-Z0-9_]+\z/
+
+ when 'groups' then true if !value.empty? &&
+ value !~ /\A[a-zA-Z0-9_\s\,]+\z/
+ else
+ false
+ end
+ end
+
+end
@@ -0,0 +1,35 @@
+require File.expand_path('../helper', __FILE__)
+
+class Commandoio < Service::TestCase
+ def setup
+ @stubs = Faraday::Adapter::Test::Stubs.new
+ end
+
+ def test_push
+ @stubs.post "v1/recipes/_______mock_recipe_______/execute" do |env|
+ body = Faraday::Utils.parse_query(env[:body])
+ payload = JSON.parse(body['payload'])
+
+ assert_equal 'api.commando.io', env[:url].host
+ assert_equal 'https', env[:url].scheme
+ assert_match 'application/x-www-form-urlencoded', env[:request_headers]['content-type']
+ assert_equal payload, JSON.parse(Faraday::Utils.parse_query(env[:body])['payload'])
+ assert_equal basic_auth('demo', 'skey_abcdsupersecretkey'),
+ env[:request_headers]['authorization']
+ [200, {}, '']
+ end
+
+ svc = service :push, {
+ 'api_token_secret_key' => 'skey_abcdsupersecretkey',
+ 'account_alias' => 'demo',
+ 'recipe' => '_______mock_recipe_______',
+ 'server' => '_server_',
+ 'notes' => 'Test the mock recipe!'
+ }
+ svc.receive_event
+ end
+
+ def service(*args)
+ super Service::Commandoio, *args
+ end
+end