pike 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pike/env.rb ADDED
@@ -0,0 +1,129 @@
1
+ module Pike
2
+ class Env
3
+ attr_reader \
4
+ :lifecycle, # Contains the lifecycle (an array of [tasks, params])
5
+ :variables, # Conains all environment specific variables
6
+ :name # Name of the environment
7
+
8
+ # Aboslute path to the bundle executable on the remote machine
9
+ attr_reader :bundler_prefix
10
+
11
+
12
+ ##
13
+ ## Constructor
14
+ ##
15
+
16
+ def initialize(name, &block)
17
+ @name = name
18
+ @variables = {}
19
+ @lifecycle = []
20
+ DSL::Environment.load(self, &block)
21
+ end
22
+
23
+
24
+ ##
25
+ ## Called from the DSL to add a task to the lifecycle
26
+ ##
27
+
28
+ def add_task(name, params = {})
29
+ @lifecycle << [name, params]
30
+ end
31
+
32
+
33
+ ##
34
+ ## Sets some internal variables
35
+ ##
36
+
37
+ def prepare
38
+ # Set the release path
39
+ deploy_to = get_var(:deploy_to) + '/.pike/releases/'
40
+ set_var release_path: (deploy_to.gsub('//', '/') + Time.new.to_i.to_s + '/')
41
+ set_var root_dir: File.expand_path(Dir.pwd)
42
+ set_var build_dir: File.expand_path("#{Dir.pwd}/.pikebuild")
43
+
44
+ # Ensure the ssh connection works
45
+ SSH::Connection.connect get_var(:host), get_var(:user)
46
+
47
+ # Ensure sudo works if required
48
+ if get_var(:use_sudo)
49
+ SSH::Connection.try_sudo
50
+ end
51
+
52
+ # Get bundler path
53
+ pre = "[[ -s '/etc/profile.d/rvm.sh' ]] && source /etc/profile.d/rvm.sh"
54
+ @bundler_prefix = "#{pre} && "
55
+ end
56
+
57
+
58
+ ##
59
+ ## Runs a single task from that environment
60
+ ##
61
+ def run_single_task(name)
62
+ prepare
63
+
64
+ task_to_run = nil
65
+ lifecycle.each do |task|
66
+ if task[0] == name
67
+ task_to_run = task
68
+ break
69
+ end
70
+ end
71
+
72
+ if task_to_run == nil
73
+ Main.error("Unkown task '#{name}'")
74
+ else
75
+ run_task(task_to_run[0], task_to_run[1])
76
+ end
77
+ end
78
+
79
+
80
+ ##
81
+ ## Run the complete lifecycle of the environment
82
+ ##
83
+
84
+ def run_lifecycle
85
+ prepare
86
+
87
+ @lifecycle.each do |task|
88
+ run_task task[0], task[1]
89
+ end
90
+ end
91
+
92
+
93
+ ##
94
+ ## Called from the DSL to set variables
95
+ ##
96
+
97
+ def set_var(vars = {})
98
+ vars.keys.each do |key|
99
+ @variables[key] = vars[key]
100
+ end
101
+ end
102
+
103
+
104
+ ##
105
+ ## Returns a variables value
106
+ ##
107
+
108
+ def get_var(key)
109
+ @variables[key] || Main.get_var(key)
110
+ end
111
+
112
+
113
+ ##
114
+ ## Runs a task for that environment
115
+ ##
116
+
117
+ def run_task(name, params)
118
+ Main.action "# Run task #{name}" do
119
+ task = Main.find_task(name)
120
+
121
+ if task
122
+ task.run(self, params)
123
+ else
124
+ raise "Task #{name} not found"
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,86 @@
1
+ module Pike
2
+ class Logger
3
+ class << self
4
+
5
+ ##
6
+ ## Call this if Main.get(:log_file) is finally determined
7
+ ##
8
+
9
+ def init
10
+ setup_logfile
11
+
12
+ log(@log_buffer.string) if @log_buffer
13
+
14
+ log
15
+ log("Timestamp: #{Time.now}")
16
+ log
17
+ log('Starting build ...')
18
+ log
19
+ end
20
+
21
+
22
+ ##
23
+ ## Logs a String or (if no param is given) a line break.
24
+ ## Can be called before Logger.init, then everything will be buffered and written to the
25
+ ## log file after it is initialized
26
+ ##
27
+
28
+ def log(what = ' ')
29
+ if @log
30
+ log_target = @log
31
+ else
32
+ @log_buffer = StringIO.new unless @log_buffer
33
+ log_target = @log_buffer
34
+ end
35
+
36
+ log_target.puts "#{what}\n"
37
+ log_target.flush
38
+ end
39
+
40
+
41
+ ##
42
+ ## The banner or header for the log file containing some debug infos
43
+ ##
44
+
45
+ def log_banner
46
+ log('#######################################')
47
+ log('# #')
48
+ log('# PIKE >> Intelligent Deployments #')
49
+ log('# #')
50
+ log('#######################################')
51
+ log
52
+ log("Pike version: #{Pike::VERSION.to_s}")
53
+ log("Ruby version: #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} on #{RUBY_PLATFORM}")
54
+ log
55
+ log('Documentation: https://github.com/phortx/pike/wiki')
56
+ log('Issues: https://github.com/phortx/pike')
57
+ log
58
+ end
59
+
60
+
61
+ ##
62
+ ## Closes the log file
63
+ ##
64
+
65
+ def close
66
+ @log.close if @log
67
+ end
68
+
69
+
70
+
71
+ private #-------------------------------------------------------------------------------------
72
+
73
+ ##
74
+ ## Setup the log file
75
+ ##
76
+
77
+ def setup_logfile
78
+ log_file = Main.get_var(:log_file)
79
+ FileUtils.mkdir_p(File.dirname(log_file))
80
+ FileUtils.rm_f(log_file)
81
+ FileUtils.touch(log_file)
82
+ @log = File.open(log_file, 'w')
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/pike/main.rb ADDED
@@ -0,0 +1,275 @@
1
+ require 'pike/env'
2
+ require 'pike/version'
3
+ require 'pike/logger'
4
+ require 'pike/ssh/connection'
5
+ require 'pike/dsl/pikefile'
6
+ require 'pike/dsl/task'
7
+ require 'pike/dsl/environment'
8
+ require 'pike/tasks/task'
9
+ require 'pike/tasks/command'
10
+ require 'pike/rba/builder'
11
+ require 'fileutils'
12
+ require 'stringio'
13
+
14
+ # Main class manages the whole pike system
15
+
16
+ module Pike
17
+ class Main
18
+ class << self
19
+ attr_accessor :debug
20
+
21
+ attr_reader \
22
+ :env, # Current environment to deploy
23
+
24
+ # Hash of available environments
25
+ :envs,
26
+
27
+ # Hash of global variables
28
+ :variables,
29
+
30
+ # Hash with all available tasks
31
+ :tasks,
32
+
33
+ # The task the user wants to run, nil means default lifecycle for that env
34
+ :task_to_run,
35
+
36
+ # Dry run?
37
+ :dry_run,
38
+
39
+ # Verbose mode?
40
+ :verbose
41
+
42
+
43
+ ##
44
+ ## Constructor
45
+ ##
46
+
47
+ def init(options)
48
+ @envs = {}
49
+ @variables = {}
50
+ @tasks = {}
51
+ @debug = options[:debug]
52
+ @task_to_run = nil
53
+ @dry_run = options[:dry_run]
54
+ @verbose = options[:verbose]
55
+
56
+ Logger.log_banner
57
+
58
+ # Variable defaults
59
+ set_var ({
60
+ branch: :master,
61
+ log_file: 'log/pike.log'
62
+ })
63
+
64
+ # Some pretty utf8 chars to illustrate some actions
65
+ @config_char = "\u26a1"
66
+ @environment_char = "\u2699"
67
+ end
68
+
69
+
70
+ ##
71
+ ## Run the deployment
72
+ ##
73
+
74
+ def run
75
+ begin
76
+ # Step 1: Check if the Pikefile exists and load config
77
+ load_pikefile
78
+
79
+ # step 2: Internal stuff
80
+ Logger.init
81
+
82
+ # Step 3: Set environment
83
+ set_environment
84
+
85
+ # Step 4: Load tasks.rb
86
+ prepare
87
+
88
+ # Step 5: Run deployment lifecycle for the given environment
89
+ run_lifecycle
90
+
91
+ # Step 6: Close SSH connection
92
+ finalize
93
+ rescue Exception => e
94
+ Main.error(e.message, e.backtrace)
95
+ end
96
+ end
97
+
98
+
99
+ ##
100
+ ## Load Pikefile
101
+ ##
102
+
103
+ def load_pikefile
104
+ action "#{@config_char} Loading Pikefile" do
105
+ # Check if file exists
106
+ raise 'No Pikefile found' unless File.exist?(Dir.pwd + '/Pikefile')
107
+
108
+ # Load the Pikefile
109
+ DSL::Pikefile.load('Pikefile')
110
+ end
111
+ end
112
+
113
+
114
+ ##
115
+ ## Sets the @env variable depending on what the user provided
116
+ ##
117
+
118
+ def set_environment
119
+ action "#{@config_char} Check given environment" do
120
+ @env = ARGV[0].to_sym
121
+
122
+ if @envs.include?(@env)
123
+ @env = @envs[@env]
124
+ Logger.log "Environment: #{@env.name}"
125
+ else
126
+ Logger.log "Unknown environment #{@env}. Please check you Pikefile."
127
+ raise "Unknown environment #{@env}. Please check you Pikefile."
128
+ end
129
+
130
+ if ARGV[1]
131
+ @task_to_run = ARGV[1].to_sym
132
+ Logger.log "Task: #{@task_to_run}"
133
+ end
134
+
135
+ @env
136
+ end
137
+ end
138
+
139
+
140
+ ##
141
+ ## Load tasks
142
+ ##
143
+
144
+ def prepare
145
+ action "#{@config_char} Preparing ..." do
146
+ Logger.log("Loading tasks ...")
147
+ DSL::Pikefile.load(File.expand_path(File.dirname(__FILE__)) + '/tasks/tasks.rb')
148
+
149
+ true
150
+ end
151
+ end
152
+
153
+
154
+ ##
155
+ ## Run the lifecycle for the environment
156
+ ##
157
+
158
+ def run_lifecycle
159
+ if @task_to_run
160
+ Logger.log "Run task #{@task_to_run} for env '#{@env.name}'"
161
+ action "#{@environment_char} Run task #{@task_to_run} for env '#{@env.name}'" do
162
+ @env.run_single_task(@task_to_run)
163
+ end
164
+ else
165
+ Logger.log "Run deployment lifecycle for env '#{@env.name}'"
166
+ action "#{@environment_char} Run deployment lifecycle for env '#{@env.name}'" do
167
+ @env.run_lifecycle
168
+ end
169
+ end
170
+ end
171
+
172
+
173
+ ##
174
+ ## Close ssh connection and logger
175
+ ##
176
+
177
+ def finalize
178
+ Logger.log
179
+ Logger.log "Finished."
180
+
181
+ SSH::Connection.close
182
+ Logger.close
183
+ end
184
+
185
+
186
+ ##
187
+ ## Adds one or more variables (called from DSL::Pikefile.set)
188
+ ##
189
+
190
+ def set_var(vars = {})
191
+ vars.keys.each do |key|
192
+ Logger.log "Set variable '#{key}' to '#{vars[key]}"
193
+ @variables[key] = vars[key]
194
+ end
195
+ end
196
+
197
+
198
+ ##
199
+ ## Get value of an variable (called from DSL::Pikefile.get)
200
+ ##
201
+
202
+ def get_var(key)
203
+ @variables[key]
204
+ end
205
+
206
+
207
+ ##
208
+ ## Add a task (called from DSL::Pikefile.task)
209
+ ##
210
+
211
+ def add_task(name, &block)
212
+ Logger.log "Adding task '#{name}'"
213
+ @tasks[name] = Task.new(name, &block)
214
+ end
215
+
216
+
217
+ ##
218
+ ## Add a environment (called from DSL::Pikefile.env)
219
+ ##
220
+ def add_env(name, &block)
221
+ Logger.log "Adding environment '#{name}'"
222
+ @envs[name] = Env.new(name, &block)
223
+ end
224
+
225
+
226
+ ##
227
+ ## Find task by name and return
228
+ ##
229
+
230
+ def find_task(task)
231
+ @tasks[task]
232
+ end
233
+
234
+
235
+ ##
236
+ ## Wrapper for step method to set @debug
237
+ ##
238
+
239
+ def action(msg, vital = true, &block)
240
+ begin
241
+ step msg, vital: vital, debug: @debug do
242
+ yield block
243
+ end
244
+ rescue Exception => e
245
+ Main.error(e.message, e.backtrace)
246
+ end
247
+ end
248
+
249
+
250
+ ##
251
+ ## Handles an error
252
+ ##
253
+
254
+ def error(msg, trace = nil)
255
+ puts
256
+ puts
257
+ puts msg
258
+ puts
259
+
260
+ if @debug
261
+ puts trace || caller
262
+ end
263
+
264
+ puts
265
+
266
+ Logger.log
267
+ Logger.log
268
+ Logger.log "Error in task:"
269
+ Logger.log msg
270
+ Logger.log
271
+ exit 1
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,60 @@
1
+ require 'rubygems/package'
2
+ require 'fileutils'
3
+
4
+ module Pike
5
+ module RBA
6
+ class Builder
7
+ ##
8
+ ## Builds an .rba file
9
+ ##
10
+
11
+ def self.build(target_file)
12
+ FileUtils.touch(target_file)
13
+ tarfile = File.open(target_file, 'w')
14
+
15
+ tar_stream = StringIO.new
16
+ tar_stream = build_tar_file(tar_stream)
17
+ gzipped_tar_stream = gzip(tar_stream)
18
+
19
+ tarfile.write gzipped_tar_stream.string
20
+
21
+ tarfile.close
22
+ end
23
+
24
+
25
+ private
26
+
27
+ def self.build_tar_file(tarfile)
28
+ Gem::Package::TarWriter.new(tarfile) do |tar|
29
+ files = Dir.glob('**/*', File::FNM_DOTMATCH).reject! {|f| f =~ /^tmp|log/i || f =~ /^\.\.?$/ }
30
+
31
+ files.each do |file|
32
+ mode = File.stat(file).mode
33
+ relative_file = file.sub /^#{Regexp::escape(Dir.pwd)}\/?/, ''
34
+
35
+ if File.directory?(file)
36
+ tar.mkdir relative_file, mode
37
+ else
38
+ Logger.log " Adding file to rba: #{file}"
39
+
40
+ tar.add_file relative_file, mode do |tf|
41
+ File.open(file, "rb") { |f| tf.write f.read }
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ return tarfile
48
+ end
49
+
50
+ def self.gzip(stream)
51
+ gzipped_stream = StringIO.new
52
+ z = Zlib::GzipWriter.new(gzipped_stream)
53
+ z.write stream.string
54
+ z.close
55
+
56
+ return StringIO.new gzipped_stream.string
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,179 @@
1
+ require 'net/ssh/shell'
2
+ require 'net/scp'
3
+ require 'pike/ssh/runner'
4
+
5
+ module Pike
6
+ module SSH
7
+ class Connection
8
+ class << self
9
+ # Net::SSH session
10
+ attr_reader :session
11
+
12
+ # Current working directory
13
+ attr_reader :cwd
14
+
15
+ # Host we're connected with
16
+ attr_reader :host
17
+
18
+ # User which was used to authenticate
19
+ attr_reader :user
20
+
21
+ # Current state of the connection
22
+ attr_reader :state
23
+
24
+ # The sudo password for that connection
25
+ attr_accessor :sudo_password
26
+
27
+
28
+ ##
29
+ ## Connects via ssh to target host if not already connected
30
+ ##
31
+
32
+ def connect(host, user)
33
+ @host = host
34
+ @user = user
35
+ @cwd = '~'
36
+
37
+ unless @session
38
+ Main.action "Connecting to #{host}" do
39
+ begin
40
+ @session = ::Net::SSH.start(@host, @user)
41
+ wait!
42
+ true
43
+ rescue Exception => e
44
+ Main.error(e.inspect)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+
51
+ ##
52
+ ## Try's sudo to ensure the password prompt will not pop again
53
+ ##
54
+
55
+ def try_sudo
56
+ if @session
57
+ Main.action "Trying sudo" do
58
+ result = run("sudo echo 'ok'")
59
+
60
+ if result.success?
61
+ if result.stdout.strip.end_with?('ok')
62
+ true
63
+ else
64
+ Main.error('Unexpected data received while trying sudo: ' + result.stderr)
65
+ end
66
+ else
67
+ Main.error('sudo failed. Wrong password?')
68
+ end
69
+ end
70
+
71
+ false
72
+ else
73
+ connection_error
74
+ end
75
+ end
76
+
77
+
78
+ ##
79
+ ## Copies a file via scp to the remote server
80
+ ##
81
+
82
+ def scp(local, remote, host)
83
+ if ready?
84
+ Logger.log "Uploading file #{local} to #{host}:#{remote} ..."
85
+
86
+ begin
87
+ @session.scp.upload!(File.expand_path(local), remote)
88
+ rescue Exception => e
89
+ Main.error('Something went wrong with scp.', e)
90
+ end
91
+ else
92
+ connection_error
93
+ end
94
+ end
95
+
96
+
97
+ ##
98
+ ## Executes a command
99
+ ##
100
+
101
+ def run(cmd)
102
+ connection_error unless ready?
103
+ Runner.run(self, cmd)
104
+ end
105
+
106
+
107
+ ##
108
+ ## Closes the connection
109
+ ##
110
+
111
+ def close
112
+ if @session
113
+ begin
114
+ @session.close
115
+ rescue
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ ##
122
+ ## Wait method, wrapper for Net::SSH::Session.loopt
123
+ ##
124
+
125
+ def wait!
126
+ @session.loop
127
+ end
128
+
129
+
130
+ ##
131
+ ## Checks if the class is ready to run commands
132
+ ##
133
+
134
+ def ready?
135
+ @session != nil
136
+ end
137
+
138
+
139
+
140
+ ##
141
+ ## Changes the woring directory
142
+ ##
143
+
144
+ def cd(new_cwd)
145
+ prc = run("cd #{new_cwd}")
146
+
147
+ if prc.success?
148
+ @cwd = new_cwd
149
+ else
150
+ Main.error("Can't change workind directory: #{new_cwd} (#{prc.stderr.strip})")
151
+ end
152
+ end
153
+
154
+
155
+ ##
156
+ ## Wrapper for Session.open_channel
157
+ ##
158
+
159
+ def open_channel(&method)
160
+ @session.open_channel &method
161
+ end
162
+
163
+
164
+
165
+
166
+ private
167
+
168
+
169
+ ##
170
+ ## Error for unestablished connections
171
+ ##
172
+
173
+ def connection_error
174
+ Main.error('SSH connection is not ready yet. Call Pike::SSH::Connection.connect first.')
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end