pike 0.0.1

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