ridoku 0.1.0

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.
@@ -0,0 +1,195 @@
1
+
2
+
3
+ module Ridoku
4
+ class ConfigWizard
5
+ attr_accessor :fields
6
+
7
+ def initialize
8
+ self.fields = {
9
+ service_role_arn: :arn_string,
10
+ instance_profile_arn: :arn_string,
11
+ ssh_key: :path
12
+ }
13
+ end
14
+
15
+ def run
16
+ $stdout.puts $stdout.colorize(ConfigWizard.help_text, :bold)
17
+
18
+ $stdout.puts "Do you wish to run the wizard now? [#{$stdout.colorize('Y', :bold)}|n]"
19
+ res = $stdin.gets
20
+ if res.match(%r(^[Nn]))
21
+ Base.config[:local_init] = true
22
+ Base.save_config(::RUNCOM)
23
+ exit 1
24
+ end
25
+
26
+ sra_info = <<-EOF
27
+ Service Role ARN is used to access OpsWorks and issue commands on your
28
+ behalf. No suitable role was found.
29
+
30
+ Please enter the appropriate Role used to issue commands on OpsWorks.
31
+ Leave the field blank to attempt to generate one or to refresh from account
32
+ credentials. 's' or 'skip' if you wish to use existing values.
33
+
34
+ #{$stdout.colorize('Current Service ARN:', [:white, :bold])} #{$stdout.colorize(Base.config[:service_arn], :green)}
35
+ EOF
36
+ inst_info = <<-EOF
37
+ Instance Profile ARN is used to for each instance created for OpsWorks.
38
+
39
+ Please enter the appropriate default Role used for instance profiles.
40
+ Leave the field blank to attempt to generate one or to refresh from account
41
+ credentials. 's' or 'skip' if you wish to use existing values.
42
+
43
+ #{$stdout.colorize('Current Instance ARN:', [:white, :bold])} #{$stdout.colorize(Base.config[:instance_arn], :green)}
44
+ EOF
45
+ info = {
46
+ service_role_arn: sra_info,
47
+ instance_profile_arn: inst_info
48
+ }
49
+
50
+ ConfigWizard.fetch_input(Base.config, fields, info)
51
+
52
+ case Base.config[:service_role_arn]
53
+ when ''
54
+ Base.config.delete(:service_arn)
55
+ Base.configure_service_roles
56
+ when 's', 'skip'
57
+ else
58
+ Base.config[:service_arn] = Base.config[:service_role_arn]
59
+ end
60
+
61
+ case Base.config[:instance_profile_arn]
62
+ when ''
63
+ Base.config.delete(:instance_arn)
64
+ Base.configure_instance_roles
65
+ when 's', 'skip'
66
+ else
67
+ Base.config[:instance_arn] = Base.config[:instance_profile_arn]
68
+ end
69
+
70
+ Base.config[:local_init] = true
71
+ Base.save_config(::RUNCOM)
72
+
73
+ $stdout.puts 'Updating required Security Groups'
74
+ Base.update_pg_security_groups_in_all_regions
75
+
76
+ $stdout.puts 'Configuration complete.'
77
+ exit 0
78
+ end
79
+ protected
80
+
81
+ class << self
82
+
83
+ def help_text
84
+ help = <<-EOF
85
+ Configuration Wizard:
86
+
87
+ In order to get ridoku configured with your OpsWorks account, Ridoku must
88
+ collect pertinent required info. The wizard can be run at any time after the
89
+ first with the command line option of `--wizard`.
90
+
91
+ Values to be configured:
92
+ ssh_key:
93
+ Path to the SSH key to be used for git repositories
94
+ (cook books, apps, etc). It is recommended that this be generated
95
+ separately from your personal SSH keys so that they can be revoked
96
+ effecting other logins.
97
+
98
+ service_role_arn:
99
+ If a valid service_role_arn cannot be found, Ridoku will attempt to
100
+ generate one for you. If you've already used OpsWorks, Ridoku should be
101
+ able to find the necessary Roles for you.
102
+
103
+ instance_role_arn:
104
+ If a valid instance_role_arn cannot be found, Ridoku will attempt to
105
+ generate one for you. If you've already used OpsWorks, Ridoku should be
106
+ able to find the necessary Roles for you.
107
+ EOF
108
+ end
109
+
110
+ def fetch_input(output, required = {}, info = {})
111
+ info ||= {}
112
+ info.merge!({
113
+ ssh_key: <<-EOF
114
+ Key files (such as SSH keys) should be provided by file path. In the case of
115
+ GIT repository SSH keys (custom cookbooks, application respository), these
116
+ should be to the private keys. I recommend generating keys specifically for
117
+ each use, so that the keys can be easily tracked and removed if necessary,
118
+ without requiring you replace your keys on every machine you access.
119
+ Enter 's' or 'skip' if you wish to keep the currently configured value.
120
+ EOF
121
+ })
122
+
123
+ recurse_required(required, output, info)
124
+ end
125
+
126
+ protected
127
+
128
+ # Recurse through required hash to collected all necessary information.
129
+ def recurse_required(req, user, info, buffer = '')
130
+ req.each do |k,v|
131
+ if v.is_a?(Hash)
132
+ buffer += info_for(k, info)
133
+ buffer += "In #{$stdout.colorize(k, :bold)}:"
134
+ recurse_required(v, user[k] ||= {}, info, buffer)
135
+ buffer = ''
136
+ else
137
+ next if user[k]
138
+ $stdout.puts '-'*80
139
+ $stdout.puts buffer if buffer.length > 0
140
+ info_block = info_for(k, info)
141
+ $stdout.puts info_block if info_block.length > 0
142
+ $stdout.puts "For #{$stdout.colorize(k, :bold)} (#{$stdout.colorize(v, :red)}):"
143
+ get_response(user, k, v)
144
+ end
145
+ end
146
+ end
147
+
148
+ # Handle getting the response from the user and validating input.
149
+ def get_response(user, key, value)
150
+ done = false
151
+ while !done
152
+ begin
153
+ val = validate_response(key, value, $stdin.gets.chomp)
154
+ user[key] = val unless val == :skip
155
+ done = true
156
+ rescue ArgumentError => e
157
+ $stdout.puts e.to_s
158
+ $stdout.puts 'Retry?'
159
+ res = $stdin.gets
160
+ done = (res.match(/^(Y|y)/) == nil)
161
+ end
162
+ end
163
+ end
164
+
165
+ # Validate input is as expected.
166
+ def validate_response(key, expect, value)
167
+ return :skip if value.match(%r(s|skip))
168
+
169
+ case key
170
+ when :ssh_key
171
+ value.gsub!(%r(^~), ENV['HOME']) if value.match(%r(^~))
172
+
173
+ fail ArgumentError.new('Invalid input provided.') unless
174
+ File.exists?(value) || value == ''
175
+ end
176
+
177
+ case expect
178
+ when :arn_string
179
+ fail ArgumentError.new('Invalid ARN provided.') unless
180
+ value.match(/^.*:.*:.*:.*:([0-9]+):.*$||/)
181
+ when :array
182
+ value = value.split(%r([^\\](:|\|)))
183
+ end
184
+
185
+ value
186
+ end
187
+
188
+ # Print warning info associated with a particular attributes.
189
+ def info_for(key, info)
190
+ return $stdout.colorize(info[key], :bold) if info.key?(key)
191
+ ''
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,103 @@
1
+ #
2
+ # Command: cook
3
+ #
4
+
5
+ require 'rugged'
6
+ require 'ridoku/base'
7
+
8
+ module Ridoku
9
+ register :cook
10
+
11
+ class Cook < Base
12
+
13
+ def run
14
+ command = Base.config[:command]
15
+ sub_command = (command.length > 0 && command[1]) || nil
16
+
17
+ Base.fetch_stack
18
+
19
+ case sub_command
20
+ # when 'list', nil
21
+ # list
22
+ when 'update'
23
+ update
24
+ when 'run'
25
+ cook
26
+ else
27
+ print_cook_help
28
+ end
29
+ end
30
+
31
+ protected
32
+
33
+ def list
34
+ update unless File.exists?(cookbook_path)
35
+ end
36
+
37
+ def update
38
+ $stdout.puts "Updating custom cookbooks..."
39
+
40
+ Base.run_command(Base.update_cookbooks(Base.extract_instance_ids))
41
+ end
42
+
43
+ class << self
44
+ def valid_recipe_format?(recipes)
45
+ return false if recipes.length == 0
46
+
47
+ recipe = %r(^[a-zA-Z0-9_\-]+(::[a-zA-Z0-9_\-]+){0,1}$)
48
+ recipes.each do |cr|
49
+ return false unless cr.match(recipe)
50
+ end
51
+
52
+ true
53
+ end
54
+
55
+ def cook_recipe(recipes, custom_json = nil)
56
+ cook_recipe_on_layers(recipes, nil, custom_json)
57
+ end
58
+
59
+ def cook_recipe_on_layers(recipes, layers, custom_json = nil)
60
+ Base.fetch_app()
61
+
62
+ recipes = [recipes] unless recipes.is_a?(Array)
63
+
64
+ unless valid_recipe_format?(recipes)
65
+ $stderr.puts 'Invalid recipes provided.'
66
+ print_cook_help
67
+ exit 1
68
+ end
69
+
70
+ instance_ids = Base.extract_instance_ids(layers)
71
+
72
+ if instance_ids.length == 0
73
+ $stderr.puts 'No valid instances available.'
74
+ exit 1
75
+ end
76
+
77
+ $stdout.puts "Running recipes:"
78
+ recipes.each do |arg|
79
+ $stdout.puts " #{$stdout.colorize(arg, :green)}"
80
+ end
81
+
82
+ command = Base.execute_recipes(Base.app[:app_id], instance_ids,
83
+ Base.config[:comment], recipes, custom_json)
84
+
85
+ Base.run_command(command)
86
+ end
87
+ end
88
+
89
+ def cook
90
+ Ridoku::Cook.cook_recipe(ARGV)
91
+ end
92
+
93
+ def print_cook_help
94
+ $stderr.puts <<-EOF
95
+ Command: cook
96
+
97
+ List/Modify the current app's associated domains.
98
+ cook:run run a specific or set of 'cookbook::recipe'
99
+ cook:update update the specified instance 'cookboooks'
100
+ EOF
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,101 @@
1
+ #
2
+ #
3
+
4
+ require 'ridoku/base'
5
+
6
+ module Ridoku
7
+ register :create
8
+
9
+ class Create < Base
10
+
11
+ def run
12
+ command = Base.config[:command]
13
+ sub_command = (command.length > 0 && command[1]) || nil
14
+ type = (command.length > 1 && command[2]) || nil
15
+
16
+ case sub_command
17
+ when 'app'
18
+ app(type || 'rails')
19
+ else
20
+ print_create_help
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ def configure_from_cwd(opt)
27
+ return unless File.exists?('.git')
28
+
29
+ `git remote -v | grep fetch`.match(%r(origin\s*(git@[^\s]*).*)) do |m|
30
+ opt[:type] = 'git'
31
+ opt[:url] = m[1]
32
+ $stdout.puts "Setting Git Application Source from environment:"
33
+ $stdout.puts "Url: #{$stdout.colorize(m[1], :green)}"
34
+ end
35
+ end
36
+
37
+ def print_create_help
38
+ $stderr.puts <<-EOF
39
+ Command: create
40
+
41
+ List/Modify the current layer's package dependencies.
42
+ create show this help
43
+ create:app[:rails] <name> create a new app on the --stack
44
+
45
+ Currently, if the stack does not exist for a particular app type, you
46
+ will have to create it manually.
47
+ EOF
48
+ end
49
+
50
+ def app(type)
51
+ Base.fetch_stack
52
+ Base.fetch_app
53
+
54
+ unless ARGV.length > 0
55
+ $stderr.puts $stderr.colorize('App name not specified', :red)
56
+ print_create_help
57
+ exit 1
58
+ end
59
+
60
+ existing = Base.app_list.select do |app|
61
+ app[:name] == ARGV[0]
62
+ end
63
+
64
+ if existing.length > 0
65
+ $stderr.puts $stderr.colorize("App #{ARGV[0]} already exists", :red)
66
+ print_create_help
67
+ exit 1
68
+ end
69
+
70
+ config = {
71
+ type: type,
72
+ name: ARGV[0],
73
+ shortname: ARGV[0].downcase.gsub(%r([^a-z0-9]), '-')
74
+ }
75
+
76
+ config.tap do |opt|
77
+ opt[:domains] = Base.config[:domains] if Base.config.key?(:domains)
78
+ opt[:app_source] = {}.tap do |as|
79
+ configure_from_cwd(as)
80
+ as[:ssh_key] = Base.config[:ssh_key] if Base.config.key?(:ssh_key)
81
+ end
82
+ opt[:attributes] = {}.tap do |atr|
83
+ atr[:rails_env] = Base.config[:rails_env] if Base.config.key?(:rails_env)
84
+ end
85
+ end
86
+
87
+ appconfig = RailsDefaults.new.defaults_with_wizard(:app, config)
88
+
89
+ begin
90
+ Base.create_app(appconfig)
91
+ rescue ::ArgumentError => e
92
+ $stderr.puts e.to_s
93
+ end
94
+ end
95
+
96
+ def instance
97
+ $stderr.puts 'Create instance not yet implemented.'
98
+ $stderr.puts 'Create an instance for a given layer using OpsWorks Dashboard.'
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,227 @@
1
+ # encoding: utf-8
2
+
3
+ #
4
+ # Command: cron
5
+ #
6
+
7
+ # [:day, :hour, :minute, :month, :weekday, :path, :type, :action]
8
+
9
+ require 'ridoku/base'
10
+
11
+ module Ridoku
12
+ register :cron
13
+
14
+ class Cron < Base
15
+ attr_accessor :cron
16
+
17
+ def run
18
+ command = Base.config[:command]
19
+ sub_command = (command.length > 0 && command[1]) || nil
20
+
21
+ load_environment
22
+
23
+ case sub_command
24
+ when 'list', nil
25
+ list
26
+ when 'set', 'add', 'update'
27
+ add
28
+ when 'delete'
29
+ delete
30
+ when 'remove'
31
+ remove
32
+ else
33
+ print_cron_help
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def load_environment
40
+ Base.fetch_stack
41
+ Base.fetch_instance('workers')
42
+
43
+ @default_instance = Base.instances.first[:hostname]
44
+ self.cron = (Base.custom_json['deploy'][Base.config[:app].downcase]['cron'] ||= {})
45
+ end
46
+
47
+ def print_cron_help
48
+ $stderr.puts <<-EOF
49
+ Command: cron
50
+
51
+ List/Modify the current app's associated cron.
52
+ cron[:list] lists the cron jobs associated with an application.
53
+ cron:add cron job, e.g., runner:scripts/runme.rb hour:0 minute:0
54
+ cron:delete delete this specific cron job
55
+ cron:remove removes a cron from the list (run after a delete and push)
56
+ cron:push update running cron jobs
57
+
58
+ Columns Value (default: *) Label
59
+ M Minute 0-59 minute
60
+ H Hour 0-23 hour
61
+ DM Day of Month 0-30 day_of_month
62
+ MO Month 0-11 month
63
+ DW Day of Week 0-6 day_of_week
64
+
65
+ All cron jobs are run on the Workers layer or the AssetMaster on the
66
+ Rails Application layer if one is not set, unless otherwise specified.
67
+
68
+ Example 'cron:list' output:
69
+ Type Scripts M H DM MO DW
70
+ runner scripts/runme.rb 0 0 * * *
71
+
72
+ The above list output indicates a DelayedJob cron and two separate
73
+ delayed_job processes. One processing 'mail'; the other processing 'sms'.
74
+
75
+ examples:
76
+ $ cron
77
+ No cron specified!
78
+ $ cron:add type:runner path:scripts/runme.rb hour:0 minute:0
79
+ $ cron:add type:runner path:scripts/runme_also.rb minute:*/5 instance:mukujara
80
+ $ cron:list
81
+ Type Scripts M H DM MO DW Instance
82
+ runner scripts/runme.rb 0 0 * * * oiwa
83
+ runner scripts/runme_also.rb */5 * * * * mukujara
84
+ $ cron:delete scrips/runme_also.rb
85
+ Type Scripts M H DM MO DW
86
+ runner scripts/runme.rb 0 0 * * * oiwa
87
+ delete scripts/runme_also.rb - - - - - mukujara
88
+ EOF
89
+ end
90
+
91
+ def list
92
+ if cron.length == 0
93
+ $stdout.puts 'No cron jobs specified!'
94
+ else
95
+
96
+ columns = {
97
+ type: 'Type',
98
+ path: 'Scripts',
99
+ minute: 'M',
100
+ hour: 'H',
101
+ day_of_month: 'DM',
102
+ month: 'MO',
103
+ day_of_week: 'DW',
104
+ instance: 'Instance'
105
+ }
106
+
107
+ offset = {}
108
+
109
+ self.cron.each do |cr|
110
+ columns.keys.each do |key|
111
+ skey = key.to_s
112
+ cr[skey] = '*' unless cr.key?(skey)
113
+ val = cr[skey].length
114
+ offset[key] = val if cr.key?(skey) && val > (offset[key] || 0)
115
+ end
116
+ end
117
+
118
+ columns.keys.each do |key|
119
+ offset[key] = columns[key].length if
120
+ columns[key].length > (offset[key] || 0)
121
+ end
122
+
123
+ $stdout.puts $stdout.colorize(line(offset, columns), :bold)
124
+ self.cron.each { |cr| $stdout.puts line(offset, cr, columns.keys) }
125
+ end
126
+ rescue =>e
127
+ puts e.backtrace
128
+ puts e
129
+ end
130
+
131
+ def add
132
+
133
+ croninfo = {}
134
+ cronindex = nil
135
+
136
+ ARGV.each do |cron|
137
+ info = cron.split(':',2)
138
+ croninfo[info[0].to_s] = info[1]
139
+ cronindex = get_path_index(info[1]) if info[0] == 'path'
140
+ end
141
+
142
+ croninfo['instance'] = @default_instance unless croninfo.key?('instance')
143
+
144
+ if cronindex.nil?
145
+ self.cron << croninfo
146
+ else
147
+ self.cron[cronindex] = croninfo
148
+ end
149
+
150
+ list
151
+ Base.save_stack
152
+ end
153
+
154
+ def delete
155
+ return print_cron_help unless ARGV.length > 0
156
+ cronindex = get_path_index(ARGV.first)
157
+
158
+ if cronindex.nil?
159
+ $stdout.puts $stdout.colorize(
160
+ 'Unable to find the specified script path in the cron list.', :red)
161
+ return list
162
+ end
163
+
164
+ cr = self.cron[cronindex]
165
+ cr['type'] = 'delete'
166
+ cr['minute'] = '-'
167
+ cr['hour'] = '-'
168
+ cr['day_of_month'] = '-'
169
+ cr['month'] = '-'
170
+ cr['day_of_week'] = '-'
171
+
172
+ list
173
+ Base.save_stack
174
+ end
175
+
176
+ def remove
177
+ return print_cron_help unless ARGV.length > 0
178
+ cronindex = get_path_index(ARGV.first)
179
+
180
+ if cronindex.nil?
181
+ $stdout.puts $stdout.colorize(
182
+ 'Unable to find the specified script path in the cron list.', :red)
183
+ return list
184
+ end
185
+
186
+ self.cron.delete_at(cronindex)
187
+
188
+ list
189
+ Base.save_stack
190
+ end
191
+
192
+ protected
193
+
194
+ def get_path_index(path)
195
+ cron.each_with_index do |cr, idx|
196
+ return idx if cr['path'] == path
197
+ end
198
+
199
+ return nil
200
+ end
201
+
202
+ def line(offset, columns, keys = nil)
203
+ keys ||= columns.keys
204
+ output = ''
205
+
206
+ keys.each do |key|
207
+ skey = key.to_s
208
+ content = columns[key] || columns[skey]
209
+ if skey == 'type'
210
+ case content
211
+ when 'delete'
212
+ output += $stdout.colorize(content, :red)
213
+ when 'runner'
214
+ output += $stdout.colorize(content, :green)
215
+ else
216
+ output += $stdout.colorize(content, :yellow)
217
+ end
218
+ else
219
+ output += content
220
+ end
221
+ output += ' '*(offset[key] - content.length + 2)
222
+ end
223
+
224
+ output
225
+ end
226
+ end
227
+ end