hsdeploy 0.9.6

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/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
+