stackmate 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +20 -0
- data/README.md +144 -0
- data/lib/stackmate/classmap.rb +25 -0
- data/lib/stackmate/logging.rb +28 -0
- data/lib/stackmate/participants/cloudstack.rb +202 -0
- data/lib/stackmate/participants/common.rb +64 -0
- data/lib/stackmate/stack.rb +100 -0
- data/lib/stackmate/stack_executor.rb +51 -0
- data/lib/stackmate/version.rb +3 -0
- data/lib/stackmate/waitcondition_server.rb +32 -0
- data/lib/stackmate.rb +4 -0
- data/stackmate.gemspec +31 -0
- metadata +152 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Chiradeep Vittal
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
'Software'), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
|
2
|
+
# Stackmate - CloudFormation for CloudStack
|
3
|
+
|
4
|
+
A library designed to read existing CloudFormation templates
|
5
|
+
and execute them on a CloudStack deployment.
|
6
|
+
Uses the [ruote](http://ruote.rubyforge.org) workflow engine,
|
7
|
+
and embeds a modular [Sinatra](http://www.sinatrarb.com/) application for wait handles.
|
8
|
+
|
9
|
+
Unlike CloudFormation, it does not run as a web application.
|
10
|
+
Instead it runs everything on the client side.
|
11
|
+
|
12
|
+
[Stacktician](https://github.com/chiradeep/stacktician) embeds stackmate to run it as a web application.
|
13
|
+
|
14
|
+
Note that only Basic Zone (aka EC2-Classic) is supported for now.
|
15
|
+
|
16
|
+
Follow:
|
17
|
+
* \#cloudstack-dev on Freenode
|
18
|
+
* <http://cloudstack.apache.org/mailing-lists.html>
|
19
|
+
* [@chiradeep](http://twitter.com/chiradeep) on Twitter
|
20
|
+
|
21
|
+
## Dependencies
|
22
|
+
|
23
|
+
stackmate uses [Bundler](http://gembundler.com/) to setup and maintain its
|
24
|
+
environment. Before running stackmate for the first time you need to install
|
25
|
+
Bundler (gem install bundler) and then run:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
$ bundle install
|
29
|
+
|
30
|
+
```
|
31
|
+
|
32
|
+
Bundler will download all the required gems and install them for you.
|
33
|
+
|
34
|
+
Have a look at the Gemfile if you want to see the various dependencies.
|
35
|
+
|
36
|
+
## Getting started quickly
|
37
|
+
|
38
|
+
### Using the source
|
39
|
+
|
40
|
+
* Get the source files using git
|
41
|
+
|
42
|
+
```bash
|
43
|
+
$ git clone http://github.com/chiradeep/stackmate.git
|
44
|
+
$ cd stackmate
|
45
|
+
```
|
46
|
+
|
47
|
+
* Make sure every dependency is resolved.
|
48
|
+
|
49
|
+
```bash
|
50
|
+
$ bundle install
|
51
|
+
```
|
52
|
+
* Find your API key and secret key and the url for CloudStack
|
53
|
+
|
54
|
+
For example
|
55
|
+
|
56
|
+
```bash
|
57
|
+
$ export APIKEY=upf7L-tvcHFCSYhKhw-45l9IfaKXNQSWf0nXyWye6eqOBpLT5TqN8XQGeuloV3LbSwD6zuucz22L233Nrqg2pg
|
58
|
+
$ export SECKEY=9iSsuImdUxU0oumHu0p11li4IoUtwcvrSHcU63ZHS_y-4Iz3w5xPROzyjZTUXkhI9E7dy0r3vejzgCmaQfI-yw
|
59
|
+
$ export URL="http://localhost:8080/client/api"
|
60
|
+
```
|
61
|
+
|
62
|
+
* The supplied templates are taken from the AWS samples.
|
63
|
+
|
64
|
+
You need a couple of mappings from AWS ids to your CloudStack implementation:
|
65
|
+
|
66
|
+
```bash
|
67
|
+
$ cat local.yaml
|
68
|
+
service_offerings : {'m1.small' : '13954c5a-60f5-4ec8-9858-f45b12f4b846'}
|
69
|
+
templates : {'ami-1b814f72': '7fc2c704-a950-11e2-8b38-0b06fbda5106'}
|
70
|
+
```
|
71
|
+
|
72
|
+
* Ensure you have a ssh keypair called 'Foo' (used in the template parameter below) for your account FIRST:
|
73
|
+
|
74
|
+
```bash
|
75
|
+
$ cloudmonkey
|
76
|
+
☁ Apache CloudStack 🐵 cloudmonkey 4.1.0-snapshot3. Type help or ? to list commands.
|
77
|
+
|
78
|
+
> create sshkeypair name='Foo'
|
79
|
+
```
|
80
|
+
|
81
|
+
|
82
|
+
* Create a LAMP stack:
|
83
|
+
|
84
|
+
```bash
|
85
|
+
bin/stackmate MYSTACK01 --template-file=templates/LAMP_Single_Instance.template -p "DBName=cloud;DBUserName=cloud;SSHLocation=75.75.75.0/24;DBUsername=cloud;DBPassword=cloud;DBRootPassword=cloud;KeyName=Foo"
|
86
|
+
```
|
87
|
+
|
88
|
+
* If everything is successful, stackmate will hang after deploying the security groups and vms.
|
89
|
+
You should see an output like this:
|
90
|
+
|
91
|
+
```bash
|
92
|
+
Your pre-signed URL is: http://localhost:4567/waitcondition/20130425-0706-kerujere-punopapa/WaitHandle
|
93
|
+
Try: curl -X PUT --data 'foo' http://localhost:4567/waitcondition/20130425-0706-kerujere-punopapa/WaitHandle
|
94
|
+
```
|
95
|
+
Executing the curl should unblock the wait handle. The idea of course is that the instance boots up, and reads its userdata and calls the same URL.
|
96
|
+
|
97
|
+
If you don't want the wait condition server to run, just use '-n'. Stackmate will not hang with this flag.
|
98
|
+
|
99
|
+
If you want to validate your template, you can use the --dry-run option. This will parse and validate the template and create an execution schedule.
|
100
|
+
|
101
|
+
## TODO
|
102
|
+
* Parallelize (with ruote concurrence) where possible
|
103
|
+
* rollback on error
|
104
|
+
* timeouts
|
105
|
+
* embed in a web app ( [Stacktician](https://github.com/chiradeep/stacktician) )
|
106
|
+
* add support for Advanced Zone templates (VPC), LB, etc
|
107
|
+
|
108
|
+
## Feedback & bug reports
|
109
|
+
|
110
|
+
Feedback and bug reports are welcome on the [mailing-list](dev@cloudstack.apache.org), or on the `#cloudstack-dev` IRC channel at Freenode.net.
|
111
|
+
|
112
|
+
## License
|
113
|
+
|
114
|
+
(The MIT License)
|
115
|
+
|
116
|
+
Copyright (c) 2013 Chiradeep Vittal
|
117
|
+
|
118
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
119
|
+
a copy of this software and associated documentation files (the
|
120
|
+
'Software'), to deal in the Software without restriction, including
|
121
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
122
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
123
|
+
permit persons to whom the Software is furnished to do so, subject to
|
124
|
+
the following conditions:
|
125
|
+
|
126
|
+
The above copyright notice and this permission notice shall be
|
127
|
+
included in all copies or substantial portions of the Software.
|
128
|
+
|
129
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
130
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
131
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
132
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
133
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
134
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
135
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
136
|
+
|
137
|
+
## libraries used
|
138
|
+
|
139
|
+
- ruote, <http://ruote.rubyforge.org/>
|
140
|
+
- sinatra, <http://www.sinatrarb.com/>
|
141
|
+
- cloudstack_ruby_client, <https://github.com/chipchilders/cloudstack_ruby_client>
|
142
|
+
|
143
|
+
Many thanks to the authors
|
144
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module StackMate
|
2
|
+
PROFILES = ['CLOUDSTACK', 'NOOP']
|
3
|
+
@profile = 'CLOUDSTACK'
|
4
|
+
|
5
|
+
CS_CLASS_MAP = {
|
6
|
+
'AWS::CloudFormation::WaitConditionHandle' => 'StackMate::WaitConditionHandle',
|
7
|
+
'AWS::CloudFormation::WaitCondition' => 'StackMate::WaitCondition',
|
8
|
+
'AWS::EC2::Instance' => 'StackMate::CloudStackInstance',
|
9
|
+
'AWS::EC2::SecurityGroup' => 'StackMate::CloudStackSecurityGroup'
|
10
|
+
}
|
11
|
+
|
12
|
+
def StackMate.class_for(cf_resource)
|
13
|
+
case @profile
|
14
|
+
when 'CLOUDSTACK'
|
15
|
+
return CS_CLASS_MAP[cf_resource]
|
16
|
+
when 'NOOP'
|
17
|
+
return 'StackMate::NoOpResource'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def StackMate.configure(profile)
|
22
|
+
@profile = profile
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module StackMate
|
4
|
+
module Logging
|
5
|
+
def logger
|
6
|
+
@logger ||= Logging.logger_for(self.class.name)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Use a hash class-ivar to cache a unique Logger per class:
|
10
|
+
@loggers = {}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def logger_for(classname)
|
14
|
+
@loggers[classname] ||= configure_logger_for(classname)
|
15
|
+
end
|
16
|
+
|
17
|
+
def configure_logger_for(classname)
|
18
|
+
logger = Logger.new(STDOUT)
|
19
|
+
logger.progname = classname
|
20
|
+
logger.datetime_format= '%F %T'
|
21
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
22
|
+
"[#{datetime}] #{severity} #{progname} #{msg}\n"
|
23
|
+
end
|
24
|
+
logger
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'cloudstack_ruby_client'
|
3
|
+
require 'yaml'
|
4
|
+
require 'stackmate/logging'
|
5
|
+
|
6
|
+
module StackMate
|
7
|
+
|
8
|
+
class CloudStackApiException < StandardError
|
9
|
+
def initialize(msg)
|
10
|
+
super(msg)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class CloudStackResource < Ruote::Participant
|
15
|
+
include Logging
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
def initialize()
|
20
|
+
@url = ENV['URL']
|
21
|
+
@apikey = ENV['APIKEY']
|
22
|
+
@seckey = ENV['SECKEY']
|
23
|
+
@client = CloudstackRubyClient::Client.new(@url, @apikey, @seckey, false)
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_workitem
|
27
|
+
p workitem.participant_name
|
28
|
+
reply
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def make_request(cmd, args)
|
34
|
+
begin
|
35
|
+
logger.debug "Going to make request #{cmd} to CloudStack server for resource #{@name}"
|
36
|
+
resp = @client.send(cmd, args)
|
37
|
+
jobid = resp['jobid'] if resp
|
38
|
+
resp = api_poll(jobid, 3, 3) if jobid
|
39
|
+
return resp
|
40
|
+
rescue => e
|
41
|
+
logger.error("Failed to make request #{cmd} to CloudStack server while creating resource #{@name}")
|
42
|
+
logger.error e.message + "\n " + e.backtrace.join("\n ")
|
43
|
+
raise e
|
44
|
+
rescue SystemExit
|
45
|
+
logger.error "Rescued a SystemExit exception"
|
46
|
+
raise CloudStackApiException, "Did not get 200 OK while making api call #{cmd}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def api_poll (jobid, num, period)
|
51
|
+
i = 0
|
52
|
+
loop do
|
53
|
+
break if i > num
|
54
|
+
resp = @client.queryAsyncJobResult({'jobid' => jobid})
|
55
|
+
if resp
|
56
|
+
return resp['jobresult'] if resp['jobstatus'] == 1
|
57
|
+
return {'jobresult' => {}} if resp['jobstatus'] == 2
|
58
|
+
end
|
59
|
+
sleep(period)
|
60
|
+
i += 1
|
61
|
+
end
|
62
|
+
return {}
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
class CloudStackInstance < CloudStackResource
|
68
|
+
def initialize()
|
69
|
+
super
|
70
|
+
@localized = {}
|
71
|
+
load_local_mappings()
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_workitem
|
75
|
+
myname = workitem.participant_name
|
76
|
+
@name = myname
|
77
|
+
resolved = workitem.fields['ResolvedNames']
|
78
|
+
resolved['AWS::StackId'] = workitem.fei.wfid #TODO put this at launch time
|
79
|
+
props = workitem.fields['Resources'][workitem.participant_name]['Properties']
|
80
|
+
security_group_names = []
|
81
|
+
props['SecurityGroups'].each do |sg|
|
82
|
+
sg_name = resolved[sg['Ref']]
|
83
|
+
security_group_names << sg_name
|
84
|
+
end
|
85
|
+
keypair = resolved[props['KeyName']['Ref']] if props['KeyName']
|
86
|
+
userdata = nil
|
87
|
+
if props['UserData']
|
88
|
+
userdata = user_data(props['UserData'], resolved)
|
89
|
+
end
|
90
|
+
templateid = image_id(props['ImageId'], resolved, workitem.fields['Mappings'])
|
91
|
+
templateid = @localized['templates'][templateid] if @localized['templates']
|
92
|
+
svc_offer = resolved[props['InstanceType']['Ref']] #TODO fragile
|
93
|
+
svc_offer = @localized['service_offerings'][svc_offer] if @localized['service_offerings']
|
94
|
+
args = { 'serviceofferingid' => svc_offer,
|
95
|
+
'templateid' => templateid,
|
96
|
+
'zoneid' => default_zone_id,
|
97
|
+
'securitygroupnames' => security_group_names.join(','),
|
98
|
+
'displayname' => myname,
|
99
|
+
#'name' => myname
|
100
|
+
}
|
101
|
+
args['keypair'] = keypair if keypair
|
102
|
+
args['userdata'] = userdata if userdata
|
103
|
+
resultobj = make_request('deployVirtualMachine', args)
|
104
|
+
logger.debug("Created resource #{myname}")
|
105
|
+
|
106
|
+
reply
|
107
|
+
end
|
108
|
+
|
109
|
+
def user_data(datum, resolved)
|
110
|
+
#TODO make this more general purpose
|
111
|
+
actual = datum['Fn::Base64']['Fn::Join']
|
112
|
+
delim = actual[0]
|
113
|
+
data = actual[1].map { |d|
|
114
|
+
d.kind_of?(Hash) ? resolved[d['Ref']]: d
|
115
|
+
}
|
116
|
+
Base64.urlsafe_encode64(data.join(delim))
|
117
|
+
end
|
118
|
+
|
119
|
+
def load_local_mappings()
|
120
|
+
begin
|
121
|
+
@localized = YAML.load_file('local.yaml')
|
122
|
+
rescue
|
123
|
+
print "Warning: Failed to load localized mappings from local.yaml\n"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def default_zone_id
|
128
|
+
if @localized['zoneid']
|
129
|
+
@localized['zoneid']
|
130
|
+
else
|
131
|
+
'1'
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def image_id(imgstring, resolved, mappings)
|
136
|
+
#TODO convoluted logic only handles the cases
|
137
|
+
#ImageId : {"Ref" : "FooBar"}
|
138
|
+
#ImageId : { "Fn::FindInMap" : [ "Map1", { "Ref" : "OuterKey" },
|
139
|
+
# { "Fn::FindInMap" : [ "Map2", { "Ref" : "InnerKey" }, "InnerVal" ] } ] },
|
140
|
+
#ImageId : { "Fn::FindInMap" : [ "Map1", { "Ref" : "Key" }, "Value" ] } ] },
|
141
|
+
if imgstring['Ref']
|
142
|
+
return resolved[imgstring['Ref']]
|
143
|
+
else
|
144
|
+
if imgstring['Fn::FindInMap']
|
145
|
+
key = resolved[imgstring['Fn::FindInMap'][1]['Ref']]
|
146
|
+
#print "Key = ", key, "\n"
|
147
|
+
if imgstring['Fn::FindInMap'][2]['Ref']
|
148
|
+
val = resolved[imgstring['Fn::FindInMap'][2]['Ref']]
|
149
|
+
#print "Val [Ref] = ", val, "\n"
|
150
|
+
else
|
151
|
+
if imgstring['Fn::FindInMap'][2]['Fn::FindInMap']
|
152
|
+
val = image_id(imgstring['Fn::FindInMap'][2], resolved, mappings)
|
153
|
+
#print "Val [FindInMap] = ", val, "\n"
|
154
|
+
else
|
155
|
+
val = imgstring['Fn::FindInMap'][2]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
return mappings[imgstring['Fn::FindInMap'][0]][key][val]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
class CloudStackSecurityGroup < CloudStackResource
|
167
|
+
def on_workitem
|
168
|
+
myname = workitem.participant_name
|
169
|
+
logger.debug("Going to create resource #{myname}")
|
170
|
+
@name = myname
|
171
|
+
p myname
|
172
|
+
resolved = workitem.fields['ResolvedNames']
|
173
|
+
props = workitem.fields['Resources'][myname]['Properties']
|
174
|
+
name = workitem.fields['StackName'] + '-' + workitem.participant_name;
|
175
|
+
resolved[myname] = name
|
176
|
+
args = { 'name' => name,
|
177
|
+
'description' => props['GroupDescription']
|
178
|
+
}
|
179
|
+
make_request('createSecurityGroup', args)
|
180
|
+
logger.debug("created resource #{myname}")
|
181
|
+
props['SecurityGroupIngress'].each do |rule|
|
182
|
+
cidrIp = rule['CidrIp']
|
183
|
+
if cidrIp.kind_of? Hash
|
184
|
+
#TODO: some sort of validation
|
185
|
+
cidrIpName = cidrIp['Ref']
|
186
|
+
cidrIp = resolved[cidrIpName]
|
187
|
+
end
|
188
|
+
args = { 'securitygroupname' => name,
|
189
|
+
'startport' => rule['FromPort'],
|
190
|
+
'endport' => rule['ToPort'],
|
191
|
+
'protocol' => rule['IpProtocol'],
|
192
|
+
'cidrlist' => cidrIp
|
193
|
+
}
|
194
|
+
#TODO handle usersecuritygrouplist
|
195
|
+
make_request('authorizeSecurityGroupIngress', args)
|
196
|
+
end
|
197
|
+
reply
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'stackmate/logging'
|
2
|
+
|
3
|
+
module StackMate
|
4
|
+
|
5
|
+
|
6
|
+
class WaitConditionHandle < Ruote::Participant
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
def on_workitem
|
10
|
+
myname = workitem.participant_name
|
11
|
+
logger.debug "Entering #{workitem.participant_name} "
|
12
|
+
presigned_url = 'http://localhost:4567/waitcondition/' + workitem.fei.wfid + '/' + myname
|
13
|
+
workitem.fields['ResolvedNames'][myname] = presigned_url
|
14
|
+
logger.info "Your pre-signed URL is: #{presigned_url} "
|
15
|
+
logger.info "Try: \ncurl -X PUT --data 'foo' #{presigned_url}"
|
16
|
+
WaitCondition.create_handle(myname, presigned_url)
|
17
|
+
|
18
|
+
reply
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class WaitCondition < Ruote::Participant
|
23
|
+
include Logging
|
24
|
+
@@handles = {}
|
25
|
+
@@conditions = []
|
26
|
+
def on_workitem
|
27
|
+
logger.debug "Entering #{workitem.participant_name} "
|
28
|
+
@@conditions << self
|
29
|
+
@wi = workitem
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.create_handle(handle_name, handle)
|
33
|
+
@@handles[handle_name] = handle
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_handle(handle_name)
|
37
|
+
reply(@wi) if @@handles[handle_name]
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.get_conditions()
|
41
|
+
@@conditions
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Output < Ruote::Participant
|
46
|
+
include Logging
|
47
|
+
def on_workitem
|
48
|
+
#p workitem.fields.keys
|
49
|
+
logger.debug "Entering #{workitem.participant_name} "
|
50
|
+
logger.debug "Done"
|
51
|
+
reply
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class NoOpResource < Ruote::Participant
|
56
|
+
include Logging
|
57
|
+
|
58
|
+
def on_workitem
|
59
|
+
logger.debug "Entering #{workitem.participant_name} "
|
60
|
+
reply
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'set'
|
3
|
+
require 'tsort'
|
4
|
+
require 'stackmate/logging'
|
5
|
+
|
6
|
+
module StackMate
|
7
|
+
|
8
|
+
class Stacker
|
9
|
+
include TSort
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
def initialize(templatefile, stackname, params)
|
13
|
+
@stackname = stackname
|
14
|
+
@resolved = {}
|
15
|
+
stackstr = File.read(templatefile)
|
16
|
+
@templ = JSON.parse(stackstr)
|
17
|
+
@templ['StackName'] = @stackname
|
18
|
+
@param_names = @templ['Parameters']
|
19
|
+
@deps = {}
|
20
|
+
@pdeps = {}
|
21
|
+
resolve_param_refs(params)
|
22
|
+
validate_param_values
|
23
|
+
resolve_dependencies()
|
24
|
+
@templ['ResolvedNames'] = @resolved
|
25
|
+
end
|
26
|
+
|
27
|
+
def resolve_param_refs(params)
|
28
|
+
params.split(';').each do |p|
|
29
|
+
i = p.split('=')
|
30
|
+
@resolved[i[0]] = i[1]
|
31
|
+
end
|
32
|
+
@resolved['AWS::Region'] = 'us-east-1' #TODO handle this better
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_param_values
|
36
|
+
#TODO CloudFormation parameters have validity constraints specified
|
37
|
+
#Use them to validate parameter values (e.g., email addresses)
|
38
|
+
end
|
39
|
+
|
40
|
+
def resolve_dependencies
|
41
|
+
@templ['Resources'].each { |key,val|
|
42
|
+
deps = Set.new
|
43
|
+
pdeps = Set.new
|
44
|
+
find_refs(key, val, deps, pdeps)
|
45
|
+
deps << val['DependsOn'] if val['DependsOn']
|
46
|
+
#print key, " depends on ", deps.to_a, "\n"
|
47
|
+
@deps[key] = deps.to_a
|
48
|
+
@pdeps[key] = pdeps.to_a
|
49
|
+
}
|
50
|
+
@pdeps.keys.each do |k|
|
51
|
+
unres = @pdeps[k] - @resolved.keys
|
52
|
+
if ! unres.empty?
|
53
|
+
unres.each do |u|
|
54
|
+
deflt = @param_names[u]['Default']
|
55
|
+
#print "Found default value ", deflt, " for ", u, "\n" if deflt
|
56
|
+
@resolved[u] = deflt if deflt
|
57
|
+
end
|
58
|
+
unres = @pdeps[k] - @resolved.keys
|
59
|
+
throw :unresolved, (@pdeps[k] - @resolved.keys) if !unres.empty?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def find_refs (parent, jsn, deps, pdeps)
|
66
|
+
case jsn
|
67
|
+
when Array
|
68
|
+
jsn.each {|x| find_refs(parent, x, deps, pdeps)}
|
69
|
+
#print parent, ": ", jsn, "\n"
|
70
|
+
when Hash
|
71
|
+
jsn.keys.each do |k|
|
72
|
+
#TODO Fn::GetAtt
|
73
|
+
if k == "Ref"
|
74
|
+
#only resolve dependencies on other resources for now
|
75
|
+
if !@param_names.keys.index(jsn[k]) && jsn[k] != 'AWS::Region' && jsn[k] != 'AWS::StackId'
|
76
|
+
deps << jsn[k]
|
77
|
+
#print parent, ": ", deps.to_a, "\n"
|
78
|
+
else if @param_names.keys.index(jsn[k])
|
79
|
+
pdeps << jsn[k]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
find_refs(parent, jsn[k], deps, pdeps)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
return deps
|
88
|
+
end
|
89
|
+
|
90
|
+
def tsort_each_node(&block)
|
91
|
+
@deps.each_key(&block)
|
92
|
+
end
|
93
|
+
|
94
|
+
def tsort_each_child(name, &block)
|
95
|
+
@deps[name].each(&block) if @deps.has_key?(name)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'ruote'
|
2
|
+
require 'json'
|
3
|
+
require 'stackmate/stack'
|
4
|
+
require 'stackmate/logging'
|
5
|
+
require 'stackmate/classmap'
|
6
|
+
require 'stackmate/participants/cloudstack'
|
7
|
+
require 'stackmate/participants/common'
|
8
|
+
|
9
|
+
module StackMate
|
10
|
+
|
11
|
+
class StackExecutor < StackMate::Stacker
|
12
|
+
include Logging
|
13
|
+
|
14
|
+
def initialize(templatefile, stackname, params, engine, create_wait_conditions)
|
15
|
+
super(templatefile, stackname, params)
|
16
|
+
@engine = engine
|
17
|
+
@create_wait_conditions = create_wait_conditions
|
18
|
+
end
|
19
|
+
|
20
|
+
def pdef
|
21
|
+
participants = self.strongly_connected_components.flatten
|
22
|
+
#if we want to skip creating wait conditions (useful for automated tests)
|
23
|
+
participants = participants.select { |p|
|
24
|
+
StackMate.class_for(@templ['Resources'][p]['Type']) != 'StackMate::WaitCondition'
|
25
|
+
} if !@create_wait_conditions
|
26
|
+
|
27
|
+
logger.info("Ordered list of participants: #{participants}")
|
28
|
+
|
29
|
+
participants.each do |p|
|
30
|
+
t = @templ['Resources'][p]['Type']
|
31
|
+
throw :unknown, t if !StackMate.class_for(t)
|
32
|
+
@engine.register_participant p, StackMate.class_for(t)
|
33
|
+
end
|
34
|
+
|
35
|
+
@engine.register_participant 'Output', 'StackMate::Output'
|
36
|
+
participants << 'Output'
|
37
|
+
@pdef = Ruote.define @stackname.to_s() do
|
38
|
+
cursor do
|
39
|
+
participants.collect{ |name| __send__(name) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def launch
|
45
|
+
wfid = @engine.launch( pdef, @templ)
|
46
|
+
@engine.wait_for(wfid)
|
47
|
+
logger.error { "engine error : #{@engine.errors.first.message}"} if @engine.errors.first
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rufus-json/automatic'
|
2
|
+
require 'ruote'
|
3
|
+
require 'ruote/storage/fs_storage'
|
4
|
+
require 'json'
|
5
|
+
require 'sinatra/base'
|
6
|
+
require 'stackmate/participants/common'
|
7
|
+
|
8
|
+
module StackMate
|
9
|
+
|
10
|
+
class WaitConditionServer < Sinatra::Base
|
11
|
+
set :static, false
|
12
|
+
set :run, true
|
13
|
+
|
14
|
+
def initialize()
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
put '/waitcondition/:wfeid/:waithandle' do
|
19
|
+
#print "Got PUT of " , params[:wfeid], ", name = ", params[:waithandle], "\n"
|
20
|
+
WaitCondition.get_conditions.each do |w|
|
21
|
+
w.set_handle(params[:waithandle].to_s)
|
22
|
+
end
|
23
|
+
'success
|
24
|
+
'
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
run! if app_file == $0
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/lib/stackmate.rb
ADDED
data/stackmate.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'stackmate/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'stackmate'
|
8
|
+
s.version = StackMate::VERSION
|
9
|
+
s.summary = "Execute CloudFormation templates on CloudStack"
|
10
|
+
s.description = "Parse and execute CloudFormation templates on CloudStack and other clouds"
|
11
|
+
s.authors = ["Chiradeep Vittal"]
|
12
|
+
s.email = 'chiradeepv@gmail.com'
|
13
|
+
s.files = Dir[
|
14
|
+
'lib/**/*.rb', 'test/**/*.rb',
|
15
|
+
'*.gemspec', '*.txt', '*.rdoc', '*.md'
|
16
|
+
]
|
17
|
+
s.homepage =
|
18
|
+
'https://github.com/chiradeep/stackmate'
|
19
|
+
s.platform = Gem::Platform::RUBY
|
20
|
+
|
21
|
+
#s.add_runtime_dependency 'ruby_parser', '~> 2.3'
|
22
|
+
s.add_runtime_dependency 'cloudstack_ruby_client', '>= 0.0.4'
|
23
|
+
s.add_runtime_dependency 'json'
|
24
|
+
s.add_runtime_dependency 'ruote', '>= 2.3.0'
|
25
|
+
s.add_runtime_dependency 'sinatra', '>= 1.4.2'
|
26
|
+
s.add_runtime_dependency 'yajl-ruby', '= 1.1.0'
|
27
|
+
|
28
|
+
s.add_development_dependency 'json'
|
29
|
+
|
30
|
+
s.require_path = 'lib'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stackmate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chiradeep Vittal
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: cloudstack_ruby_client
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.0.4
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.0.4
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: ruote
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.3.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.3.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sinatra
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.4.2
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.4.2
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: yajl-ruby
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - '='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 1.1.0
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - '='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 1.1.0
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: json
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: Parse and execute CloudFormation templates on CloudStack and other clouds
|
111
|
+
email: chiradeepv@gmail.com
|
112
|
+
executables: []
|
113
|
+
extensions: []
|
114
|
+
extra_rdoc_files: []
|
115
|
+
files:
|
116
|
+
- lib/stackmate/classmap.rb
|
117
|
+
- lib/stackmate/logging.rb
|
118
|
+
- lib/stackmate/participants/cloudstack.rb
|
119
|
+
- lib/stackmate/participants/common.rb
|
120
|
+
- lib/stackmate/stack.rb
|
121
|
+
- lib/stackmate/stack_executor.rb
|
122
|
+
- lib/stackmate/version.rb
|
123
|
+
- lib/stackmate/waitcondition_server.rb
|
124
|
+
- lib/stackmate.rb
|
125
|
+
- stackmate.gemspec
|
126
|
+
- LICENSE.txt
|
127
|
+
- README.md
|
128
|
+
homepage: https://github.com/chiradeep/stackmate
|
129
|
+
licenses: []
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ! '>='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
none: false
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 1.8.25
|
149
|
+
signing_key:
|
150
|
+
specification_version: 3
|
151
|
+
summary: Execute CloudFormation templates on CloudStack
|
152
|
+
test_files: []
|