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 +19 -0
- data/bin/hsdeploy +25 -0
- data/lib/hsdeploy.rb +4 -0
- data/lib/hsdeploy/command.rb +157 -0
- data/lib/hsdeploy/config.rb +21 -0
- data/lib/hsdeploy/target.rb +65 -0
- data/lib/hsdeploy/target/hostingstack.rb +138 -0
- data/lib/hsdeploy/target/hostingstack/api_client.rb +317 -0
- data/lib/hsdeploy/version.rb +3 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/target_spec.rb +31 -0
- metadata +177 -0
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,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
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
}
|
data/spec/target_spec.rb
ADDED
@@ -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
|
+
|