resat 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +321 -0
- data/Rakefile +33 -0
- data/bin/resat +223 -0
- data/examples/rightscale/README.rdoc +39 -0
- data/examples/rightscale/additional/run_server.yml +75 -0
- data/examples/rightscale/config/resat.yaml +34 -0
- data/examples/rightscale/scenarios/create_server.yml +48 -0
- data/examples/rightscale/scenarios/delete_server.yml +13 -0
- data/examples/rightscale/scenarios/list_servers.yml +9 -0
- data/examples/twitter/README.rdoc +50 -0
- data/examples/twitter/additional/follow.yml +15 -0
- data/examples/twitter/additional/send_message.yml +19 -0
- data/examples/twitter/config/resat.yaml +40 -0
- data/examples/twitter/output.yml +5 -0
- data/examples/twitter/scenarios/timelines.yml +31 -0
- data/examples/twitter/scenarios/tweet.yml +14 -0
- data/lib/api_request.rb +145 -0
- data/lib/config.rb +94 -0
- data/lib/engine.rb +98 -0
- data/lib/file_set.rb +33 -0
- data/lib/filter.rb +113 -0
- data/lib/guard.rb +36 -0
- data/lib/handler.rb +50 -0
- data/lib/kwalify_helper.rb +31 -0
- data/lib/log.rb +114 -0
- data/lib/net_patch.rb +15 -0
- data/lib/rdoc_patch.rb +37 -0
- data/lib/resat.rb +5 -0
- data/lib/scenario_runner.rb +203 -0
- data/lib/variables.rb +116 -0
- data/schemas/config.yaml +48 -0
- data/schemas/scenarios.yaml +169 -0
- data/schemas/variables.yaml +18 -0
- metadata +98 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
= Synopsis
|
2
|
+
|
3
|
+
This example uses the RightScale REST API:
|
4
|
+
http://wiki.rightscale.com/2._References/01-RightScale/03-RightScale_API
|
5
|
+
|
6
|
+
This example will list all your servers, create a new server using the
|
7
|
+
'Rails all-in-one' server template and delete it.
|
8
|
+
|
9
|
+
See the main README.rdoc for instructions on how to setup resat prior to
|
10
|
+
running the examples.
|
11
|
+
|
12
|
+
= How to
|
13
|
+
|
14
|
+
* Run:
|
15
|
+
|
16
|
+
$ resat scenarios -d user:<your RightScale username> -d pass:<your RightScale password> -d acct:<your RightScale account number>
|
17
|
+
|
18
|
+
* See:
|
19
|
+
|
20
|
+
$ cat output.yml
|
21
|
+
|
22
|
+
* See more:
|
23
|
+
|
24
|
+
$ vi /tmp/resat.log
|
25
|
+
|
26
|
+
or
|
27
|
+
|
28
|
+
$ vi resat.log
|
29
|
+
|
30
|
+
if <tt>/tmp</tt> does not exist
|
31
|
+
|
32
|
+
= Additional Examples
|
33
|
+
|
34
|
+
The <i>run_server</i> example in the <i>additional</i> folder will create and
|
35
|
+
launch a server in the <i>default</i> deployment and wait until it's
|
36
|
+
operational before running an operational script on it. It will then stop and
|
37
|
+
delete it. See the file <tt>additional/run_server.yml</tt>
|
38
|
+
(http://github.com/raphael/resat/blob/master/examples/rightscale/additional/run_server.yml)
|
39
|
+
for additional information.
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# This scenario will launch an All-in-on Rails server, wait for it to become
|
2
|
+
# operational, run a RightScript (Mongrels restart) and then stop the server,
|
3
|
+
# wait for it to be stopped and delete it.
|
4
|
+
#
|
5
|
+
# Note: This scenario reuses the 'create_server' scenario to create the
|
6
|
+
# Rails All-in-one server.
|
7
|
+
#
|
8
|
+
name: Launch rails servers and run RightScript
|
9
|
+
includes:
|
10
|
+
- ../scenarios/create_server
|
11
|
+
steps:
|
12
|
+
- request: # Step 1. Start server
|
13
|
+
resource: servers
|
14
|
+
id: $server_id
|
15
|
+
custom:
|
16
|
+
name: start
|
17
|
+
type: post
|
18
|
+
|
19
|
+
- request: # Step 2. Retrieve Mongrels restart RightScript
|
20
|
+
operation: index
|
21
|
+
resource: right_scripts
|
22
|
+
filters:
|
23
|
+
- name: Get Rails server template
|
24
|
+
target: body
|
25
|
+
extractors:
|
26
|
+
- field: right-scripts/right-script[name='RB mongrel_cluster (re)start v1']/href
|
27
|
+
variable: right_script_id
|
28
|
+
|
29
|
+
- request: # Step 3. Wait for server to become operational
|
30
|
+
resource: servers
|
31
|
+
id: $server_id
|
32
|
+
operation: show
|
33
|
+
guards:
|
34
|
+
- name: Wait for operational state
|
35
|
+
target: body
|
36
|
+
field: server/state
|
37
|
+
pattern: "operational"
|
38
|
+
period: 10
|
39
|
+
timeout: 900
|
40
|
+
|
41
|
+
- request: # Step 4. Restart Mongrels
|
42
|
+
resource: servers
|
43
|
+
id: $server_id
|
44
|
+
custom:
|
45
|
+
name: run_script
|
46
|
+
type: post
|
47
|
+
params:
|
48
|
+
- name: right_script
|
49
|
+
value: $right_script_id
|
50
|
+
|
51
|
+
- request: # Step 5. Stop server
|
52
|
+
resource: servers
|
53
|
+
id: $server_id
|
54
|
+
custom:
|
55
|
+
name: stop
|
56
|
+
type: post
|
57
|
+
|
58
|
+
- request: # Step 6. Wait for server to become stopped
|
59
|
+
resource: servers
|
60
|
+
id: $server_id
|
61
|
+
operation: show
|
62
|
+
guards:
|
63
|
+
- name: Wait for stop state
|
64
|
+
target: body
|
65
|
+
field: server/state
|
66
|
+
pattern: "stopped"
|
67
|
+
period: 10
|
68
|
+
timeout: 900
|
69
|
+
|
70
|
+
- request: # Step 7. Now delete the server
|
71
|
+
operation: destroy
|
72
|
+
resource: servers
|
73
|
+
id: $server_id
|
74
|
+
|
75
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Hostname used for API calls
|
2
|
+
host: moo.rightscale.com
|
3
|
+
|
4
|
+
# Port
|
5
|
+
# port: 443
|
6
|
+
|
7
|
+
# Common base URL to all API calls
|
8
|
+
base_url: api/acct/$acct
|
9
|
+
|
10
|
+
# Use HTTPS?
|
11
|
+
use_ssl: yes
|
12
|
+
|
13
|
+
# Basic auth username if any
|
14
|
+
username: $user
|
15
|
+
|
16
|
+
# Basic auth password if any
|
17
|
+
password: $pass
|
18
|
+
|
19
|
+
# Common request headers for all API calls
|
20
|
+
headers:
|
21
|
+
- name: X-API-VERSION
|
22
|
+
value: '1.0'
|
23
|
+
|
24
|
+
# Common parameters for all API calls
|
25
|
+
params:
|
26
|
+
|
27
|
+
# Global variables
|
28
|
+
variables:
|
29
|
+
|
30
|
+
# Input file
|
31
|
+
#input: variables.yml
|
32
|
+
|
33
|
+
# Output file
|
34
|
+
#output: variables.yml
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Create new Rails all-in-one server in default deployment
|
2
|
+
name: Create Rails Server
|
3
|
+
steps:
|
4
|
+
- request: # Step 1. Retrieve default deployment
|
5
|
+
operation: index
|
6
|
+
resource: deployments
|
7
|
+
valid_codes:
|
8
|
+
- 200
|
9
|
+
filters:
|
10
|
+
- name: Get default deployment
|
11
|
+
target: body
|
12
|
+
extractors:
|
13
|
+
- field: deployments/deployment/href
|
14
|
+
variable: deployment
|
15
|
+
|
16
|
+
- request: # Step 2. Retrieve Rails template
|
17
|
+
operation: index
|
18
|
+
resource: server_templates
|
19
|
+
filters:
|
20
|
+
- name: Get Rails server template
|
21
|
+
target: body
|
22
|
+
extractors:
|
23
|
+
- field: server-templates/ec2-server-template[nickname='Rails all-in-one-developer v8']/href
|
24
|
+
variable: server_template
|
25
|
+
|
26
|
+
- request: # Step 3. Create server
|
27
|
+
operation: create
|
28
|
+
resource: servers
|
29
|
+
valid_codes:
|
30
|
+
- 201
|
31
|
+
params:
|
32
|
+
- name: server[nickname]
|
33
|
+
value: 'Resat Rails Server'
|
34
|
+
- name: server[server_template_href]
|
35
|
+
value: $server_template
|
36
|
+
- name: server[deployment_href]
|
37
|
+
value: $deployment
|
38
|
+
filters:
|
39
|
+
- name: validate server response
|
40
|
+
target: body
|
41
|
+
is_empty: true
|
42
|
+
- name: Get server id
|
43
|
+
target: header
|
44
|
+
extractors:
|
45
|
+
- field: location
|
46
|
+
pattern: '.*\/(\d+)$'
|
47
|
+
variable: server_id
|
48
|
+
export: true # Export id to other scenarios
|
@@ -0,0 +1,50 @@
|
|
1
|
+
= Synopsis
|
2
|
+
|
3
|
+
This example uses the Twitter REST API:
|
4
|
+
|
5
|
+
http://apiwiki.twitter.com/REST+API+Documentation
|
6
|
+
|
7
|
+
<b>Please note</b>: This example will send a tweet on your behalf with the text:
|
8
|
+
|
9
|
+
Checking out resat (http://tinyurl.com/dg8gf9)
|
10
|
+
|
11
|
+
by default. Override the default text in the <tt>config/resat.yaml</tt> file or
|
12
|
+
via the command line:
|
13
|
+
|
14
|
+
$ resat scenarios -d tweet:'My custom tweet' -d user:... -d pass:...
|
15
|
+
|
16
|
+
See the main README.rdoc for instructions on how to setup resat prior to
|
17
|
+
running the examples.
|
18
|
+
|
19
|
+
= How to
|
20
|
+
|
21
|
+
* Run:
|
22
|
+
|
23
|
+
$ resat scenarios -d user:<your twitter username> -d pass:<your twitter password>
|
24
|
+
|
25
|
+
* See:
|
26
|
+
|
27
|
+
$ cat output.yml
|
28
|
+
|
29
|
+
* See more:
|
30
|
+
|
31
|
+
$ vi /tmp/resat.log
|
32
|
+
|
33
|
+
or
|
34
|
+
|
35
|
+
$ vi resat.log
|
36
|
+
|
37
|
+
if <tt>/tmp</tt> does not exist
|
38
|
+
|
39
|
+
= Additional Examples
|
40
|
+
|
41
|
+
The <i>additional</i> folder contains two additional scenarios which are not ran
|
42
|
+
by default:
|
43
|
+
|
44
|
+
* <tt>follow.yml</tt>: Follow given user
|
45
|
+
* <tt>send_message.yml</tt>: Send direct message to given user with given content
|
46
|
+
|
47
|
+
Both these scenarios require inputs. Inputs are given using the <tt>--define</tt>
|
48
|
+
(or <tt>-d</tt>) resat option:
|
49
|
+
|
50
|
+
$ resat additional/follow -d followed:rgsimon
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Follow username specified on command line
|
2
|
+
#
|
3
|
+
# Usage:
|
4
|
+
# resat follow -d followed:rgsimon
|
5
|
+
|
6
|
+
name: Follow given user
|
7
|
+
config: ../config/resat.yaml
|
8
|
+
steps:
|
9
|
+
- request:
|
10
|
+
resource: notifications # Act on the 'notifications' resource
|
11
|
+
custom: # Use a custom operation (i.e. not a CRUD operation)
|
12
|
+
name: follow # Operation name
|
13
|
+
type: post # POST request
|
14
|
+
id: $followed # ID of user to follow
|
15
|
+
format: xml # Get response in XML format
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Send direct message to username specified on command line
|
2
|
+
# Note: user must be following you otherwise the request returns 403.
|
3
|
+
#
|
4
|
+
# Usage:
|
5
|
+
# resat send_message -d to:rgsimon -d text:'Hello from resat!'
|
6
|
+
|
7
|
+
name: Send Direct Message
|
8
|
+
config: ../config/resat.yaml
|
9
|
+
steps:
|
10
|
+
- request:
|
11
|
+
resource: direct_messages # Act on the 'direct_messages' resource
|
12
|
+
custom: # Use a custom operation (i.e. not a CRUD operation)
|
13
|
+
name: new.xml # Operation name
|
14
|
+
type: post # POST request
|
15
|
+
params:
|
16
|
+
- name: user # 'user' parameter
|
17
|
+
value: $to # Username
|
18
|
+
- name: text # 'text' parameter
|
19
|
+
value: $text # Message content
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Hostname used for API calls
|
2
|
+
host: twitter.com
|
3
|
+
|
4
|
+
# Port
|
5
|
+
# port: 443
|
6
|
+
|
7
|
+
# Common base URL to all API calls
|
8
|
+
base_url:
|
9
|
+
|
10
|
+
# Use HTTPS?
|
11
|
+
use_ssl: yes
|
12
|
+
|
13
|
+
# Basic auth username if any
|
14
|
+
# Uses variable 'user' from the command line
|
15
|
+
username: $user
|
16
|
+
|
17
|
+
# Basic auth password if any
|
18
|
+
# Uses variable 'pass' from the command line
|
19
|
+
password: $pass
|
20
|
+
|
21
|
+
# Common request headers for all API calls
|
22
|
+
headers:
|
23
|
+
# - name: header_name
|
24
|
+
# value: header_value
|
25
|
+
|
26
|
+
# Common parameters for all API calls
|
27
|
+
params:
|
28
|
+
# - name: param_name
|
29
|
+
# value: param_value
|
30
|
+
|
31
|
+
# Global variables
|
32
|
+
variables:
|
33
|
+
- name: tweet
|
34
|
+
value: 'Checking out resat (http://tinyurl.com/dg8gf9)'
|
35
|
+
|
36
|
+
# Input file
|
37
|
+
# input: input.yml
|
38
|
+
|
39
|
+
# Output file
|
40
|
+
output: output.yml
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Retrieve public and private recent tweets
|
2
|
+
#
|
3
|
+
# http://twitter.com/statuses/public_timeline.format
|
4
|
+
# http://twitter.com/statuses/public_timeline.format
|
5
|
+
name: Twitter Timelines
|
6
|
+
config: ../config/resat.yaml
|
7
|
+
steps:
|
8
|
+
- request:
|
9
|
+
resource: statuses
|
10
|
+
custom: # Use a custom operation (i.e. not a CRUD operation)
|
11
|
+
name: public_timeline.xml # Operation name
|
12
|
+
type: get # GET request
|
13
|
+
filters:
|
14
|
+
- name: extract latest public tweet
|
15
|
+
target: body
|
16
|
+
extractors:
|
17
|
+
- field: statuses/status/text
|
18
|
+
variable: last public tweet
|
19
|
+
save: yes
|
20
|
+
- request:
|
21
|
+
resource: statuses
|
22
|
+
custom: # Use a custom operation (i.e. not a CRUD operation)
|
23
|
+
name: friends_timeline.xml # Operation name
|
24
|
+
type: get # GET request
|
25
|
+
filters:
|
26
|
+
- name: extract latest friends tweet
|
27
|
+
target: body
|
28
|
+
extractors:
|
29
|
+
- field: statuses/status/text
|
30
|
+
variable: last friends tweet
|
31
|
+
save: yes
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Send Tweet
|
2
|
+
# http://twitter.com/statuses/update.format
|
3
|
+
name: Send Tweet
|
4
|
+
config: ../config/resat.yaml
|
5
|
+
steps:
|
6
|
+
- request:
|
7
|
+
resource: statuses # Act on the 'notifications' resource
|
8
|
+
custom: # Use a custom operation (i.e. not a CRUD operation)
|
9
|
+
name: update.xml # Operation name
|
10
|
+
type: post # POST request
|
11
|
+
params:
|
12
|
+
- name: status
|
13
|
+
value: $tweet
|
14
|
+
|
data/lib/api_request.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# API request
|
2
|
+
# See resat.rb for usage information.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
require 'rexml/document'
|
7
|
+
require File.join(File.dirname(__FILE__), 'net_patch')
|
8
|
+
|
9
|
+
module Resat
|
10
|
+
|
11
|
+
class ApiRequest
|
12
|
+
include Kwalify::Util::HashLike
|
13
|
+
attr_reader :request, :response, :send_count, :failures
|
14
|
+
|
15
|
+
# Prepare request so 'send' can be called
|
16
|
+
def prepare
|
17
|
+
@format ||= 'xml'
|
18
|
+
@failures = []
|
19
|
+
@send_count = 0
|
20
|
+
|
21
|
+
# 1. Normalize call fields
|
22
|
+
@headers ||= []
|
23
|
+
@params ||= []
|
24
|
+
# Clone config values so we don't mess with them when expanding variables
|
25
|
+
Config.headers.each do |h|
|
26
|
+
@headers << { 'name' => h['name'].dup, 'value' => h['value'].dup }
|
27
|
+
end if Config.headers
|
28
|
+
Config.params.each do |p|
|
29
|
+
@params << { 'name' => p['name'].dup, 'value' => p['value'].dup }
|
30
|
+
end if Config.params && request_class.REQUEST_HAS_BODY
|
31
|
+
Variables.substitute!(@params)
|
32
|
+
Variables.substitute!(@headers)
|
33
|
+
|
34
|
+
# 2. Build URI
|
35
|
+
Variables.substitute!(@id) if @id
|
36
|
+
uri_class = (@use_ssl || @use_ssl.nil? && Config.use_ssl) ? URI::HTTPS : URI::HTTP
|
37
|
+
port = @port || Config.port || uri_class::DEFAULT_PORT
|
38
|
+
@uri = uri_class.build( :host => @host || Config.host, :port => port )
|
39
|
+
base_url = "/#{@base_url || Config.base_url}/".squeeze('/')
|
40
|
+
Variables.substitute!(base_url)
|
41
|
+
path = "#{base_url}#{@resource}"
|
42
|
+
path = "#{path}/#{@id}" if @id
|
43
|
+
path = "#{path}.#{@format}" if @format && !@custom
|
44
|
+
path = "#{path}#{@custom.separator}#{@custom.name}" if @custom
|
45
|
+
@uri.merge!(path)
|
46
|
+
|
47
|
+
# 3. Build request
|
48
|
+
case @operation
|
49
|
+
when 'index', 'show' then request_class = Net::HTTP::Get
|
50
|
+
when 'create' then request_class = Net::HTTP::Post
|
51
|
+
when 'update' then request_class = Net::HTTP::Put
|
52
|
+
when 'destroy' then request_class = Net::HTTP::Delete
|
53
|
+
else
|
54
|
+
if @custom
|
55
|
+
case @custom.type
|
56
|
+
when 'get' then request_class = Net::HTTP::Get
|
57
|
+
when 'post' then request_class = Net::HTTP::Post
|
58
|
+
when 'put' then request_class = Net::HTTP::Put
|
59
|
+
when 'delete' then request_class = Net::HTTP::Delete
|
60
|
+
end
|
61
|
+
else
|
62
|
+
@failures << "Missing request operation for request on '#{@resource}'."
|
63
|
+
return
|
64
|
+
end
|
65
|
+
end
|
66
|
+
@request = request_class.new(@uri.to_s)
|
67
|
+
username = @username || Config.username
|
68
|
+
Variables.substitute!(username) if username
|
69
|
+
password = @password || Config.password
|
70
|
+
Variables.substitute!(password) if password
|
71
|
+
if username && password
|
72
|
+
@request.basic_auth(username, password)
|
73
|
+
end
|
74
|
+
form_data = Hash.new
|
75
|
+
@headers.each { |header| @request[header['name']] = header['value'] }
|
76
|
+
@params.each { |param| form_data[param['name']] = param['value'] }
|
77
|
+
@request.set_form_data(form_data) unless form_data.empty?
|
78
|
+
Log.request(@request)
|
79
|
+
|
80
|
+
# 4. Send request and check response code
|
81
|
+
@oks = @valid_codes.map { |r| r.to_s } if @valid_codes
|
82
|
+
@oks ||= %w{200 201 202 203 204 205 206}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Actually send the request
|
86
|
+
def send
|
87
|
+
sleep(delay_seconds) # Delay request if needed
|
88
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
89
|
+
http.use_ssl = Config.use_ssl
|
90
|
+
begin
|
91
|
+
res = http.start { |http| @response = http.request(@request) }
|
92
|
+
rescue Exception => e
|
93
|
+
@failures << "Exception raised while making request: #{e.message}"
|
94
|
+
end
|
95
|
+
if @failures.size == 0
|
96
|
+
@send_count += 1
|
97
|
+
if @oks.include?(res.code)
|
98
|
+
Log.response(@response)
|
99
|
+
else
|
100
|
+
Log.response(@response, false)
|
101
|
+
@failures << "Request returned #{res.code}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Does response include given header or body field?
|
107
|
+
def has_response_field?(field, target)
|
108
|
+
return unless @response
|
109
|
+
return @response.key?(field) if target == 'header'
|
110
|
+
doc = REXML::Document.new(@response.body) rescue nil
|
111
|
+
return doc && !doc.elements[field].nil?
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get value of response header or body field
|
115
|
+
def get_response_field(field, target)
|
116
|
+
return unless @response
|
117
|
+
return @response[field] if target == 'header'
|
118
|
+
doc = REXML::Document.new(@response.body)
|
119
|
+
elem = doc.elements[field]
|
120
|
+
return elem.get_text.to_s if elem
|
121
|
+
end
|
122
|
+
|
123
|
+
protected
|
124
|
+
|
125
|
+
# Calculate number of seconds to wait before sending request
|
126
|
+
def delay_seconds
|
127
|
+
seconds = nil
|
128
|
+
if delay = @delay || Config.delay
|
129
|
+
min_delay = max_delay = nil
|
130
|
+
if delay =~ /([\d]+)\.\.([\d]+)/
|
131
|
+
min_delay = Regexp.last_match[1].to_i
|
132
|
+
max_delay = Regexp.last_match[2].to_i
|
133
|
+
elsif delay.to_i.to_s == delay
|
134
|
+
min_delay = max_delay = delay.to_i
|
135
|
+
end
|
136
|
+
if min_delay && max_delay
|
137
|
+
seconds = rand(max_delay - min_delay + 1) + min_delay
|
138
|
+
end
|
139
|
+
end
|
140
|
+
seconds || 0
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|