stackmate 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|