hsdeploy 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # hsdeploy - HostingStack Deploy Tool
2
+
3
+ Deployment tool for the HostingStack open-source PaaS, with special support for multi-stage deploys (e.g. for staging environments).
4
+
5
+ ## Deploying a Ruby on Rails app
6
+
7
+ gem install hsdeploy
8
+ hsdeploy add production young-samurai-4@example.org
9
+ hsdeploy production
10
+
11
+ ## Config file
12
+
13
+ hsdeploy keeps a local config file .hsdeployrc within the top-level sourcecode directory (determined by location of .git directory).
14
+
15
+ ## Legalese
16
+
17
+ Copyright (c) 2011, 2012 Efficient Cloud Ltd.
18
+
19
+ Released under the [MIT license](http://www.opensource.org/licenses/mit-license.php).
data/bin/hsdeploy ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'logger'
7
+ require 'rubygems'
8
+ require 'deploytool'
9
+ require 'deploytool/command'
10
+
11
+ $logger = Logger.new STDOUT
12
+ $logger.formatter = proc do |severity, datetime, progname, msg|
13
+ if severity == "ERROR"
14
+ "ERROR: #{msg}\n"
15
+ else
16
+ "#{msg}\n"
17
+ end
18
+ end
19
+ HighLine.track_eof = false
20
+
21
+ args = ARGV.dup
22
+ ARGV.clear
23
+ command = args.shift.strip rescue 'help'
24
+
25
+ DeployTool::Command.run(command, args)
data/lib/hsdeploy.rb ADDED
@@ -0,0 +1,4 @@
1
+ module HSDeploy; end
2
+
3
+ require 'hsdeploy/config'
4
+ require 'hsdeploy/target'
@@ -0,0 +1,157 @@
1
+ require 'hsdeploy/version'
2
+
3
+ class HSDeploy::Command
4
+ COMMANDS = ["to", "logs", "import", "export", "config", "run"]
5
+
6
+ def self.print_help
7
+ puts "HostingStack Deploytool Version #{HSDeploy::VERSION} Usage Instructions"
8
+ puts ""
9
+ puts "Add a target:"
10
+ puts " hsdeploy add production young-samurai-4@example.org"
11
+ puts " hsdeploy add staging green-flower-2@example.org"
12
+ puts ""
13
+ puts "Deploy the current directory to the target:"
14
+ puts " hsdeploy production"
15
+ puts ""
16
+ puts "Run a command on the server:"
17
+ puts " hsdeploy run production rake db:migrate"
18
+ end
19
+
20
+ def self.find_target(target_name)
21
+ unless (target = HSDeploy::Config[target_name]) && !target.nil? && target.size > 0
22
+ puts "ERROR: Target \"#{target_name}\" is not configured"
23
+ puts ""
24
+ print_help
25
+ exit
26
+ end
27
+ [target_name, HSDeploy::Target.from_config(target)]
28
+ end
29
+
30
+ def self.handle_target_exception(e)
31
+ $logger.debug e.inspect
32
+ $logger.debug e.backtrace
33
+ $logger.info "\nAn Error (%s) occured. Please contact %s support: %s" % [e.inspect, HSDeploy::Target::HostingStack.cloud_name, HSDeploy::Target::HostingStack.support_email]
34
+ exit 2
35
+ end
36
+
37
+ def self.run(command, args)
38
+ if args.include?("--debug")
39
+ args.delete("--debug")
40
+ $logger.level = Logger::DEBUG
41
+ elsif args.include?("-d")
42
+ args.delete("-d")
43
+ $logger.level = Logger::DEBUG
44
+ elsif args.include?("-v")
45
+ args.delete("-v")
46
+ $logger.level = Logger::DEBUG
47
+ else
48
+ $logger.level = Logger::INFO
49
+ end
50
+
51
+ change_to_toplevel_dir!
52
+
53
+ HSDeploy::Config.load(".hsdeployrc")
54
+
55
+ if command == "help"
56
+ print_help
57
+ elsif command == "add"
58
+ if args[0].nil?
59
+ puts "ERROR: Missing target name."
60
+ puts ""
61
+ puts "Use \"deploy help\" if you're lost."
62
+ exit
63
+ end
64
+ if args[1].nil?
65
+ puts "ERROR: Missing target specification."
66
+ puts ""
67
+ puts "Use \"deploy help\" if you're lost."
68
+ exit
69
+ end
70
+ unless target = HSDeploy::Target.find(args[1])
71
+ puts "ERROR: Couldn't find provider for target \"#{args[1]}\""
72
+ puts ""
73
+ puts "Use \"deploy help\" if you're lost."
74
+ exit
75
+ end
76
+ if target.respond_to?(:verify)
77
+ target.verify
78
+ end
79
+ HSDeploy::Config[args[0]] = target.to_h
80
+ elsif command == "list"
81
+ puts "Registered Targets:"
82
+ HSDeploy::Config.all.each do |target_name, target|
83
+ target = HSDeploy::Target.from_config(target)
84
+ puts " %s%s" % [target_name.ljust(15), target.to_s]
85
+ end
86
+ elsif command == "run"
87
+ target_name, target = find_target args.shift
88
+ begin
89
+ command = args.join(' ').strip
90
+ if command.empty?
91
+ puts "ERROR: Must specify command to be run.\n\n"
92
+ print_help
93
+ exit 2
94
+ end
95
+ target.exec(command)
96
+ rescue => e
97
+ handle_target_exception e
98
+ end
99
+ else
100
+ args.unshift command unless command == "to"
101
+ target_name, target = find_target args.shift
102
+
103
+ opts = {}
104
+ opts[:timing] = true if args.include?("--timing")
105
+
106
+ begin
107
+ target.push(opts)
108
+ rescue => e
109
+ handle_target_exception e
110
+ end
111
+ end
112
+
113
+ if target_name and target
114
+ HSDeploy::Config[target_name] = target.to_h
115
+ end
116
+
117
+ HSDeploy::Config.save
118
+ rescue Net::HTTPServerException => e
119
+ $logger.info "ERROR: HTTP call returned %s %s" % [e.response.code, e.response.message]
120
+ if target
121
+ $logger.debug "\nTarget:"
122
+ target.to_h.each do |k, v|
123
+ next if k.to_sym == :password
124
+ $logger.debug " %s = %s" % [k, v]
125
+ end
126
+ end
127
+ $logger.debug "\nBacktrace:"
128
+ $logger.debug " " + e.backtrace.join("\n ")
129
+ $logger.debug "\nResponse:"
130
+ e.response.each_header do |k, v|
131
+ $logger.debug " %s: %s" % [k, v]
132
+ end
133
+ $logger.debug "\n " + e.response.body.gsub("\n", "\n ")
134
+ $logger.info "\nPlease run again with \"--debug\" and report the output at http://j.mp/hsdeploy-issue"
135
+ exit 2
136
+ end
137
+
138
+ # Tries to figure out if we're running in a subdirectory of the source,
139
+ # and switches to the top-level if that's the case
140
+ def self.change_to_toplevel_dir!
141
+ indicators = [".git", "Gemfile", "LICENSE", "test"]
142
+
143
+ timeout = 10
144
+ path = Dir.pwd
145
+ begin
146
+ indicators.each do |indicator|
147
+ next unless File.exists?(File.join(path, indicator))
148
+
149
+ $logger.debug "Found correct top-level directory %s, switching working directory." % [path] unless path == Dir.pwd
150
+ Dir.chdir path
151
+ return
152
+ end
153
+ end until (path = File.dirname(path)) == "/" || (timeout -= 1) == 0
154
+
155
+ $logger.debug "DEBUG: Couldn't locate top-level directory (traversed until %s), falling back to %s" % [path, Dir.pwd]
156
+ end
157
+ end
@@ -0,0 +1,21 @@
1
+ require 'inifile'
2
+
3
+ class HSDeploy::Config
4
+ def self.all
5
+ @@configfile.to_h
6
+ end
7
+ def self.[](section)
8
+ @@configfile[section]
9
+ end
10
+ def self.[]=(section, value)
11
+ @@configfile[section] = value
12
+ end
13
+
14
+ def self.load(filename)
15
+ @@configfile = IniFile.load(filename)
16
+ end
17
+
18
+ def self.save
19
+ @@configfile.save unless @@configfile.to_h.empty?
20
+ end
21
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+
3
+ class Module
4
+ def track_subclasses
5
+ instance_eval %{
6
+ def self.known_subclasses
7
+ @__deploytool_subclasses
8
+ end
9
+
10
+ def self.add_known_subclass(s)
11
+ superclass.add_known_subclass(s) if superclass.respond_to?(:inherited_tracking_subclasses)
12
+ (@__deploytool_subclasses ||= []) << s
13
+ end
14
+
15
+ def self.inherited_tracking_subclasses(s)
16
+ add_known_subclass(s)
17
+ inherited_not_tracking_subclasses(s)
18
+ end
19
+ alias :inherited_not_tracking_subclasses :inherited
20
+ alias :inherited :inherited_tracking_subclasses
21
+ }
22
+ end
23
+ end
24
+
25
+ class HSDeploy::Target
26
+ track_subclasses
27
+
28
+ def self.find(target_spec)
29
+ known_subclasses.each do |klass|
30
+ next unless klass.matches?(target_spec)
31
+ return klass.create(target_spec)
32
+ end
33
+ nil
34
+ end
35
+
36
+ def self.from_config(config)
37
+ known_subclasses.each do |klass|
38
+ next unless klass.to_s.split('::').last == config['type']
39
+ return klass.new(config)
40
+ end
41
+ nil
42
+ end
43
+
44
+ def self.get_json_resource(url)
45
+ res = nil
46
+ begin
47
+ timeout(5) do
48
+ res = Net::HTTP.get_response(Addressable::URI.parse(url))
49
+ end
50
+ rescue Timeout::Error
51
+ $logger.debug "Calling '%s' took longer than 5s, skipping" % [url, res.code, res.body]
52
+ return nil
53
+ end
54
+ return nil if res.nil?
55
+ if res.code != '200'
56
+ $logger.debug "Calling '%s' returned %s, skipping" % [url, res.code, res.body]
57
+ return nil
58
+ end
59
+ JSON.parse(res.body)
60
+ end
61
+ end
62
+
63
+ (Dir.glob(File.dirname(__FILE__)+'/target/*.rb') - [__FILE__]).sort.each do |f|
64
+ require 'hsdeploy/target/' + File.basename(f)
65
+ end
@@ -0,0 +1,138 @@
1
+ require 'highline'
2
+ class HSDeploy::Target::HostingStack < HSDeploy::Target
3
+ SUPPORTED_API_VERSION = 4
4
+
5
+ def self.cloud_name
6
+ @cloud_name || 'HostingStack'
7
+ end
8
+
9
+ def self.support_email
10
+ @support_email || 'maintainers@hostingstack.org'
11
+ end
12
+
13
+ def self.parse_target_spec(target_spec)
14
+ server, app_name = target_spec.split('@').reverse
15
+ if app_name.nil?
16
+ app_name = server.split('.', 2).first
17
+ end
18
+ [server, 'api.' + server, 'api.' + server.split('.', 2).last].each do |api_server|
19
+ begin
20
+ return [app_name, api_server] if check_version(api_server)
21
+ rescue => e
22
+ puts e
23
+ end
24
+ end
25
+ nil
26
+ end
27
+
28
+ def self.matches?(target_spec)
29
+ return true if parse_target_spec(target_spec)
30
+ end
31
+
32
+ def to_h
33
+ x = {:type => "HostingStack", :api_server => @api_client.server, :app_name => @api_client.app_name,}
34
+ if @api_client.auth_method == :refresh_token
35
+ x.merge({:refresh_token => @api_client.refresh_token})
36
+ else
37
+ x
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ "%s@%s (HS-based platform)" % [@api_client.app_name, @api_client.server]
43
+ end
44
+
45
+ def initialize(options)
46
+ @api_server = options['api_server']
47
+ auth = options.has_key?('refresh_token') ? {:refresh_token => options['refresh_token']} : {:email => options['email'], :password => options['password']}
48
+ @api_client = ApiClient.new(options['api_server'], options['app_name'], auth)
49
+ end
50
+
51
+ def self.check_version(api_server)
52
+ begin
53
+ info = get_json_resource("http://%s/info" % api_server)
54
+ rescue => e
55
+ $logger.debug "Exception: %s\n%s" % [e.message, e.backtrace.join("\n")]
56
+ return false
57
+ end
58
+ return false unless info && info['name'] == "hs"
59
+
60
+ if info['api_version'] > SUPPORTED_API_VERSION
61
+ $logger.error "This version of deploytool is outdated.\nThis server requires at least API Version #{info['api_version']}."
62
+ return false
63
+ end
64
+ @cloud_name = info['cloud_name']
65
+ return true
66
+ end
67
+
68
+ def self.create(target_spec)
69
+ app_name, api_server = parse_target_spec(target_spec)
70
+ HostingStack.new('api_server' => api_server, 'app_name' => app_name)
71
+ end
72
+
73
+ def verify
74
+ self.class.check_version(@api_server)
75
+ begin
76
+ info = @api_client.info
77
+ return true
78
+ rescue => e
79
+ $logger.debug "Exception: %s %s\n %s" % [e.class.name, e.message, e.backtrace.join("\n ")]
80
+ if e.message.include?("401 ")
81
+ $logger.error "Authentication failed (password wrong?)"
82
+ elsif e.message.include?("404 ")
83
+ $logger.error "Application does not exist"
84
+ elsif e.message.start_with?("ERROR")
85
+ puts e.message
86
+ $logger.info "\nPlease contact %s support and include the above output: %s" % [HostingStack.cloud_name, HostingStack.support_email]
87
+ else
88
+ $logger.error "Remote server said: %s" % [e.message]
89
+ $logger.info "\nPlease contact %s support and include the above output: %s" % [HostingStack.cloud_name, HostingStack.support_email]
90
+ end
91
+ end
92
+ exit 5
93
+ end
94
+
95
+ def push(opts)
96
+ self.class.check_version(@api_server)
97
+ info = @api_client.info
98
+ if info[:blocking_deployment]
99
+ $logger.error info[:blocking_deployment]
100
+ exit 4
101
+ end
102
+ if info[:warn_deployment]
103
+ $logger.info info[:warn_deployment]
104
+ exit 5 if HighLine.new.ask("Deploy anyway? (y/N)").downcase.strip != 'y'
105
+ end
106
+
107
+ code_token = @api_client.upload
108
+ deploy_token = @api_client.deploy(code_token)
109
+ @api_client.deploy_status(deploy_token, opts) # Blocks till deploy is done
110
+ rescue => e
111
+ if e.message.start_with?("ERROR")
112
+ puts e.message
113
+ else
114
+ $logger.debug e.backtrace.join("\n")
115
+ $logger.info "Unknown error happened: #{e.message}. Please try again or contact support."
116
+ end
117
+ end
118
+
119
+ def exec(opts)
120
+ self.class.check_version(@api_server)
121
+ info = @api_client.info
122
+ if info[:blocking_deployment]
123
+ $logger.error info[:blocking_deployment]
124
+ exit 4
125
+ end
126
+
127
+ @api_client.exec(opts) # Blocks til done
128
+ rescue => e
129
+ if e.message.start_with?("ERROR")
130
+ puts e.message
131
+ else
132
+ $logger.debug e.backtrace.join("\n")
133
+ $logger.info "Unknown error happened: #{e.message}"
134
+ end
135
+ end
136
+ end
137
+
138
+ require 'deploytool/target/hostingstack/api_client'
@@ -0,0 +1,317 @@
1
+ require 'addressable/uri'
2
+ require 'net/http'
3
+ require 'net/http/post/multipart'
4
+ require 'fileutils'
5
+ require 'tempfile'
6
+ require 'zip'
7
+ require 'oauth2'
8
+ require 'multi_json'
9
+ require 'highline'
10
+
11
+ CLIENT_ID = 'org.hostingstack.api.deploytool'
12
+ CLIENT_SECRET = '11d6b5cc70e4bc9563a3b8dd50dd34f6'
13
+
14
+ class HSDeploy::Target::HostingStack
15
+ class ApiError < StandardError
16
+ attr_reader :response
17
+ def initialize(response)
18
+ @response = response
19
+ details = MultiJson.decode(response.body) rescue nil
20
+ super("API failure: #{response.status} #{details}")
21
+ end
22
+ end
23
+
24
+ class ApiClient
25
+ attr_reader :server, :app_name, :email, :password, :refresh_token, :auth_method
26
+ def initialize(server, app_name, auth)
27
+ @app_name = app_name
28
+ @server = server
29
+ if auth.has_key? :refresh_token
30
+ @refresh_token = auth[:refresh_token]
31
+ @auth_method = :refresh_token
32
+ elsif auth.has_key? :email
33
+ @auth_method = :password
34
+ @email = auth[:email]
35
+ @password = auth[:password]
36
+ else
37
+ @auth_method = :password
38
+ end
39
+ end
40
+
41
+ def re_auth
42
+ @auth_method = :password
43
+ @auth = nil
44
+ end
45
+
46
+ def auth!
47
+ return if @auth
48
+
49
+ @client = OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET, :site => "http://#{server}/", :token_url => '/oauth2/token', :raise_errors => false) do |builder|
50
+ builder.use Faraday::Request::Multipart
51
+ builder.use Faraday::Request::UrlEncoded
52
+ builder.adapter :net_http
53
+ end
54
+
55
+ @auth = false
56
+ tries = 0
57
+ while not @auth
58
+ if tries != 0 && HighLine.new.ask("Would you like to try again? (y/n): ") != 'y'
59
+ return
60
+ end
61
+ token = nil
62
+ handled_error = false
63
+ begin
64
+ if @auth_method == :password
65
+ if !@email.nil? && !@password.nil?
66
+ # Upgrade from previous configuration file
67
+ print "Logging in..."
68
+ begin
69
+ token = @client.password.get_token(@email, @password, :raise_errors => true)
70
+ token = token.refresh!
71
+ @email = nil
72
+ @password = nil
73
+ rescue StandardError => e
74
+ @email = nil
75
+ @password = nil
76
+ tries = 0
77
+ retry
78
+ ensure
79
+ print "\r"
80
+ end
81
+ else
82
+ tries += 1
83
+ $logger.info "Please specify your %s login data" % [HSDeploy::Target::HostingStack.cloud_name]
84
+ email = HighLine.new.ask("E-mail: ")
85
+ password = HighLine.new.ask("Password: ") {|q| q.echo = "*" }
86
+ print "Authorizing..."
87
+ begin
88
+ token = @client.password.get_token(email, password, :raise_errors => true)
89
+ token = token.refresh!
90
+ ensure
91
+ print "\r"
92
+ end
93
+ puts "Authorization succeeded."
94
+ end
95
+ else
96
+ params = {:client_id => @client.id,
97
+ :client_secret => @client.secret,
98
+ :grant_type => 'refresh_token',
99
+ :refresh_token => @refresh_token
100
+ }
101
+ token = @client.get_token(params)
102
+ end
103
+ rescue OAuth2::Error => e
104
+ handled_error = true
105
+ print "Authorization failed"
106
+ token = nil
107
+ details = MultiJson.decode(e.response.body) rescue nil
108
+ if details
109
+ puts ": #{details['error_description']}"
110
+ re_auth if details['error']
111
+ else
112
+ puts "."
113
+ end
114
+ rescue EOFError
115
+ exit 1
116
+ rescue Interrupt
117
+ exit 1
118
+ rescue StandardError => e
119
+ $logger.debug "ERROR: #{e.inspect}"
120
+ $logger.info "\nAn Error occured. Please try again in a Minute or contact %s support: %s" % [HSDeploy::Target::HostingStack.cloud_name, HSDeploy::Target::HostingStack.support_email]
121
+ puts ""
122
+ tries += 1
123
+ end
124
+ @auth = token
125
+ if not token and not handled_error
126
+ puts "Authorization failed."
127
+ end
128
+ end
129
+
130
+ @refresh_token = token.refresh_token
131
+ @auth_method = :refresh_token
132
+ end
133
+
134
+ def call(method, method_name, data = {})
135
+ auth!
136
+ method_name = '/' + method_name unless method_name.nil?
137
+ url = Addressable::URI.parse("http://#{@server}/api/cli/v1/apps/#{@app_name}#{method_name}.json")
138
+ opts = method==:get ? {:params => data} : {:body => data}
139
+ opts.merge!({:headers => {'Accept' => 'application/json'}})
140
+ response = @auth.request(method, url.path, opts)
141
+ if not [200,201].include?(response.status)
142
+ raise ApiError.new(response)
143
+ end
144
+ MultiJson.decode(response.body)
145
+ end
146
+
147
+ def to_h
148
+ {:server => @server, :app_name => @app_name, :email => email, :password => @password, :refresh_token => @refresh_token, :auth_method => @auth_method}
149
+ end
150
+
151
+ def info
152
+ response = call :get, nil
153
+ return nil if not response
154
+ data = {}
155
+ response["app"].each do |k,v|
156
+ next unless v.kind_of?(String)
157
+ data[k.to_sym] = v
158
+ end
159
+ data
160
+ rescue ApiError => e
161
+ if e.response.status == 404
162
+ raise "ERROR: Application does not exist on server"
163
+ end
164
+ end
165
+
166
+ def upload
167
+ puts "-----> Packing code tarball..."
168
+
169
+ ignore_regex = [
170
+ /(^|\/).{1,2}$/,
171
+ /(^|\/).git\//,
172
+ /^.hsdeployrc$/,
173
+ /^log\//,
174
+ /(^|\/).DS_Store$/,
175
+ /(^|\/)[^\/]+\.(bundle|o|so|rl|la|a)$/,
176
+ /^vendor\/gems\/[^\/]+\/ext\/lib\//
177
+ ]
178
+
179
+ appfiles = Dir.glob('**/*', File::FNM_DOTMATCH)
180
+ appfiles.reject! {|f| File.directory?(f) }
181
+ appfiles.reject! {|f| ignore_regex.map {|r| !f[r] }.include?(false) }
182
+
183
+ # TODO: Shouldn't upload anything that's in gitignore
184
+
185
+ # Construct a temporary zipfile
186
+ tempfile = Tempfile.open("ecli-upload.zip")
187
+ Zip::ZipOutputStream.open(tempfile.path) do |z|
188
+ appfiles.each do |appfile|
189
+ z.put_next_entry appfile
190
+ z.print IO.read(appfile)
191
+ end
192
+ end
193
+
194
+ puts "-----> Uploading %s code tarball..." % human_filesize(tempfile.path)
195
+ initial_response = call :post, 'upload', {:code => Faraday::UploadIO.new(tempfile, "application/zip")}
196
+ initial_response["code_token"]
197
+ end
198
+
199
+ def deploy(code_token)
200
+ initial_response = call :post, 'deploy', {:code_token => code_token}
201
+ return nil if not initial_response
202
+ initial_response["token"]
203
+ end
204
+
205
+ def save_timing_data(data)
206
+ File.open('deploytool-timingdata-%d.json' % (Time.now), 'w') do |f|
207
+ f.puts data.to_json
208
+ end
209
+ end
210
+
211
+ def deploy_status(deploy_token, opts)
212
+ start = Time.now
213
+ timing = []
214
+ previous_status = nil
215
+ print "-----> Started deployment '%s'" % deploy_token
216
+
217
+ while true
218
+ sleep 1
219
+ resp = call :get, 'deploy_status', {:deploy_token => deploy_token}
220
+
221
+ if resp["message"].nil?
222
+ puts resp
223
+ puts "...possibly done."
224
+ break
225
+ end
226
+ if resp["message"] == 'finished'
227
+ puts "\n-----> FINISHED after %d seconds!" % (Time.now-start)
228
+ break
229
+ end
230
+
231
+ status = resp["message"].gsub('["', '').gsub('"]', '')
232
+ if previous_status != status
233
+ case status
234
+ when "build"
235
+ puts "\n-----> Building/updating virtual machine..."
236
+ when "deploy"
237
+ print "\n-----> Copying virtual machine to app hosts"
238
+ when "publishing"
239
+ print "\n-----> Updating HTTP gateways"
240
+ when "cleanup"
241
+ print "\n-----> Removing old deployments"
242
+ end
243
+ previous_status = status
244
+ end
245
+
246
+ logs = resp["logs"]
247
+ if logs
248
+ puts "" if status != "build" # Add newline after the dots
249
+ puts logs
250
+ timing << [Time.now-start, status, logs]
251
+ else
252
+ timing << [Time.now-start, status]
253
+ if status == 'error'
254
+ if logs.nil? or logs.empty?
255
+ raise "ERROR after %d seconds!" % (Time.now-start)
256
+ end
257
+ elsif status != "build"
258
+ print "."
259
+ STDOUT.flush
260
+ end
261
+ end
262
+ end
263
+ ensure
264
+ save_timing_data timing if opts[:timing]
265
+ end
266
+
267
+ def human_filesize(path)
268
+ size = File.size(path)
269
+ units = %w{B KB MB GB TB}
270
+ e = (Math.log(size)/Math.log(1024)).floor
271
+ s = "%.1f" % (size.to_f / 1024**e)
272
+ s.sub(/\.?0*$/, units[e])
273
+ end
274
+
275
+ def cancel_exec(cli_task_id, command_id)
276
+ call :delete, "cli_tasks/#{cli_task_id}", {} unless cli_task_id.nil?
277
+ call :delete, "commands/#{command_id}", {} unless command_id.nil?
278
+ nil
279
+ end
280
+
281
+ def exec(command)
282
+ name = "cli#{Time.now}"
283
+ response = call :post, 'commands', {:command => {:name => name, :command => command}}
284
+ return nil if not response
285
+ command_id = response["command"]["id"]
286
+
287
+ response = call :post, 'cli_tasks', {:cli_task => {:name => name, :command_id => command_id}}
288
+ return nil if not response
289
+ cli_task_id = response["cli_task"]["id"]
290
+
291
+ response = call :post, "cli_tasks/#{cli_task_id}/dispatch_task", {}
292
+ return nil if not response
293
+ token = response["token"]
294
+ puts "---> Launching..."
295
+
296
+ while true do
297
+ response = call :get, "cli_tasks/#{cli_task_id}/drain_status", {:token => token}
298
+ unless response["logs"].nil?
299
+ puts response["logs"]
300
+ end
301
+ if response["message"] == "success"
302
+ puts "---> Done."
303
+ break
304
+ end
305
+ if response["message"] == "failure"
306
+ puts "---> Failed."
307
+ break
308
+ end
309
+ sleep 1
310
+ end
311
+
312
+ nil
313
+ ensure
314
+ cancel_exec cli_task_id, command_id
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,3 @@
1
+ module HSDeploy
2
+ VERSION = "0.9.6"
3
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,16 @@
1
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+
5
+ require 'rspec'
6
+ require 'hsdeploy'
7
+ require 'logger'
8
+
9
+ RSpec.configure do |config|
10
+ config.mock_with :rr
11
+ end
12
+
13
+ $logger = Logger.new File.expand_path('../spec.log', __FILE__)
14
+ $logger.formatter = proc { |severity, datetime, progname, msg|
15
+ "#{severity} #{datetime.strftime("%Y-%m-%d %H:%M:%S")}: #{msg}\n"
16
+ }
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe HSDeploy::Target do
4
+ context "Target selection" do
5
+ before do
6
+ stub.any_instance_of(HighLine).ask do |q, |
7
+ if q[/E-mail/]
8
+ "demo@hostingstack.org"
9
+ elsif q[/Password/]
10
+ "demo"
11
+ else
12
+ nil
13
+ end
14
+ end
15
+
16
+ # TODO: Mock HTTP get method
17
+ end
18
+
19
+ ["app10000@api.hostingstack.org", "app10000@hostingstack.org", "app10000@app123.hostingstack.org", "app10000.hostingstack.org"].each do |target_spec|
20
+ it "should detect #{target_spec} as an HostingStack target" do
21
+ HSDeploy::Target.find(target_spec).class.should == HSDeploy::Target::HostingStack
22
+ end
23
+ end
24
+
25
+ ["gandi.net", "1and1.com"].each do |target_spec|
26
+ it "should return an error with #{target_spec} as target" do
27
+ HSDeploy::Target.find(target_spec).class.should == NilClass
28
+ end
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hsdeploy
3
+ version: !ruby/object:Gem::Version
4
+ hash: 55
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 9
9
+ - 6
10
+ version: 0.9.6
11
+ platform: ruby
12
+ authors:
13
+ - HostingStack
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-04-09 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: inifile
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 13
29
+ segments:
30
+ - 0
31
+ - 4
32
+ - 1
33
+ version: 0.4.1
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: addressable
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: multipart-post
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ type: :runtime
63
+ version_requirements: *id003
64
+ - !ruby/object:Gem::Dependency
65
+ name: highline
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ hash: 11
73
+ segments:
74
+ - 1
75
+ - 6
76
+ - 2
77
+ version: 1.6.2
78
+ type: :runtime
79
+ version_requirements: *id004
80
+ - !ruby/object:Gem::Dependency
81
+ name: zip
82
+ prerelease: false
83
+ requirement: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ type: :runtime
93
+ version_requirements: *id005
94
+ - !ruby/object:Gem::Dependency
95
+ name: json_pure
96
+ prerelease: false
97
+ requirement: &id006 !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ type: :runtime
107
+ version_requirements: *id006
108
+ - !ruby/object:Gem::Dependency
109
+ name: oauth2
110
+ prerelease: false
111
+ requirement: &id007 !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ hash: 3
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ type: :runtime
121
+ version_requirements: *id007
122
+ description: Deployment tool for web application platforms powered by HostingStack.
123
+ email: maintainers@hostingstack.org
124
+ executables:
125
+ - hsdeploy
126
+ extensions: []
127
+
128
+ extra_rdoc_files: []
129
+
130
+ files:
131
+ - README.md
132
+ - bin/hsdeploy
133
+ - lib/hsdeploy.rb
134
+ - lib/hsdeploy/command.rb
135
+ - lib/hsdeploy/config.rb
136
+ - lib/hsdeploy/target.rb
137
+ - lib/hsdeploy/target/hostingstack.rb
138
+ - lib/hsdeploy/target/hostingstack/api_client.rb
139
+ - lib/hsdeploy/version.rb
140
+ - spec/spec.opts
141
+ - spec/spec_helper.rb
142
+ - spec/target_spec.rb
143
+ homepage: http://hostingstack.org/
144
+ licenses: []
145
+
146
+ post_install_message:
147
+ rdoc_options: []
148
+
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ hash: 3
157
+ segments:
158
+ - 0
159
+ version: "0"
160
+ required_rubygems_version: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ hash: 3
166
+ segments:
167
+ - 0
168
+ version: "0"
169
+ requirements: []
170
+
171
+ rubyforge_project:
172
+ rubygems_version: 1.8.8
173
+ signing_key:
174
+ specification_version: 3
175
+ summary: PaaS deployment tool.
176
+ test_files: []
177
+