collins_shell 0.2.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.pryrc +1 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +18 -0
  4. data/Gemfile.lock +59 -0
  5. data/README.md +335 -0
  6. data/Rakefile +64 -0
  7. data/VERSION +1 -0
  8. data/bin/collins-shell +36 -0
  9. data/collins_shell.gemspec +95 -0
  10. data/lib/collins_shell.rb +3 -0
  11. data/lib/collins_shell/asset.rb +198 -0
  12. data/lib/collins_shell/cli.rb +185 -0
  13. data/lib/collins_shell/console.rb +129 -0
  14. data/lib/collins_shell/console/asset.rb +127 -0
  15. data/lib/collins_shell/console/cache.rb +17 -0
  16. data/lib/collins_shell/console/command_helpers.rb +131 -0
  17. data/lib/collins_shell/console/commands.rb +28 -0
  18. data/lib/collins_shell/console/commands/cat.rb +123 -0
  19. data/lib/collins_shell/console/commands/cd.rb +61 -0
  20. data/lib/collins_shell/console/commands/io.rb +26 -0
  21. data/lib/collins_shell/console/commands/iterators.rb +190 -0
  22. data/lib/collins_shell/console/commands/tail.rb +178 -0
  23. data/lib/collins_shell/console/commands/versions.rb +42 -0
  24. data/lib/collins_shell/console/filesystem.rb +121 -0
  25. data/lib/collins_shell/console/options_helpers.rb +8 -0
  26. data/lib/collins_shell/errors.rb +7 -0
  27. data/lib/collins_shell/ip_address.rb +144 -0
  28. data/lib/collins_shell/ipmi.rb +67 -0
  29. data/lib/collins_shell/monkeypatch.rb +60 -0
  30. data/lib/collins_shell/provision.rb +152 -0
  31. data/lib/collins_shell/state.rb +98 -0
  32. data/lib/collins_shell/tag.rb +41 -0
  33. data/lib/collins_shell/thor.rb +209 -0
  34. data/lib/collins_shell/util.rb +120 -0
  35. data/lib/collins_shell/util/asset_printer.rb +265 -0
  36. data/lib/collins_shell/util/asset_stache.rb +32 -0
  37. data/lib/collins_shell/util/log_printer.rb +187 -0
  38. data/lib/collins_shell/util/printer_util.rb +28 -0
  39. metadata +200 -0
@@ -0,0 +1,152 @@
1
+ require 'collins_shell/thor'
2
+ require 'collins_shell/util'
3
+ require 'thor'
4
+ require 'thor/group'
5
+ require 'terminal-table'
6
+
7
+ module CollinsShell
8
+
9
+ class Provision < Thor
10
+ include ThorHelper
11
+ include CollinsShell::Util
12
+
13
+ desc 'list', 'list available provisioning profiles'
14
+ use_collins_options
15
+ def list
16
+ table = Terminal::Table.new
17
+ table.title = set_color("Provisioning Profiles", :bold, :magenta)
18
+ table << [
19
+ "Profile", "Label", "Prefix", "Pool", split("Primary Role"), split("Secondary Role"),
20
+ split("Suffix Allowed?"), split("Requires Primary Role?"),
21
+ split("Requires Secondary Role?"), split("Requires Pool?")
22
+ ]
23
+ table << :separator
24
+ data = call_collins get_collins_client, '/api/provision/profiles' do |client|
25
+ profs = client.provisioning_profiles
26
+ last = profs.size - 1
27
+ profs.each_with_index do |profile, idx|
28
+ table << [
29
+ profile.profile, split(profile.label), profile.prefix, optional_string(profile.pool),
30
+ optional_string(profile.primary_role), optional_string(profile.secondary_role),
31
+ centered(profile.suffix_allowed?), centered(profile.requires_primary_role?),
32
+ centered(profile.requires_secondary_role?), centered(profile.requires_pool?)
33
+ ]
34
+ if idx != last && too_wide? then
35
+ table << :separator
36
+ end
37
+ end
38
+ end
39
+ puts table
40
+ end
41
+
42
+ desc 'host TAG PROFILE CONTACT', 'provision host with asset tag TAG as PROFILE, and then contact CONTACT'
43
+ long_desc <<-D
44
+ Provision the host specified by the TAG argument as a PROFILE.
45
+
46
+ PROFILE must be one of the profiles found by doing a `collins-shell provision list`
47
+
48
+ CONTACT should be your hipchat username, or the username part of your email address.
49
+
50
+ This task may take up to 90 seconds to return.
51
+ D
52
+ use_collins_options
53
+ method_option :activate, :type => :boolean, :desc => 'Activate a server', :hide => true
54
+ method_option :confirm, :type => :boolean, :default => :true, :desc => 'Require confirmation. Defaults to true'
55
+ method_option :exec, :type => :string, :desc => 'Execute a command using the data from this asset. Use {{hostname}}, {{ipmi.password}}, etc for substitution'
56
+ method_option :pool, :type => :string, :desc => 'Pool for host'
57
+ method_option :primary_role, :type => :string, :desc => 'Primary role for host'
58
+ method_option :secondary_role, :type => :string, :desc => 'Secondary role for host'
59
+ method_option :suffix, :type => :string, :desc => 'Suffix to use for hostname'
60
+ method_option :verify, :type => :boolean, :default => :true, :desc => 'Verify arguments locally first'
61
+ def host tag, profile, contact
62
+ verify_profile(profile) if options.verify
63
+ config = get_collins_config
64
+ if config[:timeout] < 180 then
65
+ cclient = get_collins_client :timeout => 180
66
+ else
67
+ cclient = get_collins_client
68
+ end
69
+ asset = asset_get tag, options
70
+ if options.confirm then
71
+ printer = CollinsShell::AssetPrinter.new asset, self, :detailed => false
72
+ puts printer
73
+ require_yes "You are about to provision asset #{tag} as a #{profile}. ARE YOU SURE?", :red
74
+ end
75
+ progress_printer = Thread.new {
76
+ loop {
77
+ print "."
78
+ sleep(1)
79
+ }
80
+ }
81
+ status = call_collins cclient, "provision host" do |client|
82
+ say_status("starting", "Provisioning has started", :white)
83
+ client.provision tag, profile, contact, :activate => options.activate,
84
+ :pool => options.pool,
85
+ :primary_role => options.primary_role,
86
+ :secondary_role => options.secondary_role,
87
+ :suffix => options.suffix
88
+ end
89
+ progress_printer.terminate
90
+ puts()
91
+ if status then
92
+ say_success("Successfully provisioned asset")
93
+ asset = asset_get tag, options
94
+ printer = CollinsShell::AssetPrinter.new asset, self, :detailed => false
95
+ puts printer
96
+ asset_exec asset, options.exec, options.confirm
97
+ else
98
+ say_error("Failed to provision asset")
99
+ end
100
+ end
101
+
102
+ no_tasks do
103
+ def too_wide?
104
+ terminal_width < 150
105
+ end
106
+
107
+ def split value
108
+ if too_wide? then
109
+ value.split(' ').join("\n")
110
+ else
111
+ value
112
+ end
113
+ end
114
+
115
+ def verify_value pname, name, user_specified, profile_exists, profile_requires, profile_provides
116
+ if user_specified then
117
+ require_that(!profile_exists, "#{name} is not user configurable for '#{pname}'")
118
+ else
119
+ require_that(!profile_requires || profile_provides, "#{name} is required for '#{pname}'")
120
+ end
121
+ end
122
+
123
+ def verify_profile pname
124
+ profiles = call_collins get_collins_client, '/api/provision/profiles' do |client|
125
+ client.provisioning_profiles
126
+ end
127
+ profile = profiles.select{|p| p.profile.downcase == pname.to_s.downcase}.first
128
+ require_non_empty(profile, "No such profile '#{pname}', try collins-shell provision list")
129
+ if options.suffix? then
130
+ require_that(profile.suffix_allowed?, "Suffix not allowed for '#{pname}'")
131
+ end
132
+ verify_value pname, "Pool", options.pool?, profile.pool?, profile.requires_pool?, profile.pool
133
+ verify_value pname, "Secondary role", options.secondary_role?, profile.secondary_role?, profile.requires_secondary_role?, profile.secondary_role
134
+ verify_value pname, "Primary role", options.primary_role?, profile.primary_role?, profile.requires_primary_role?, profile.primary_role
135
+ end
136
+
137
+ def centered value
138
+ {:alignment => :center, :value => value}
139
+ end
140
+
141
+ def optional_string value, default = 'Configurable'
142
+ if value.nil? || value.empty? then
143
+ default
144
+ else
145
+ value
146
+ end
147
+ end # optional_string
148
+ end
149
+
150
+ end
151
+
152
+ end
@@ -0,0 +1,98 @@
1
+ require 'collins_shell/thor'
2
+ require 'collins_shell/util'
3
+ require 'thor'
4
+ require 'thor/group'
5
+
6
+ module CollinsShell
7
+
8
+ class State < Thor
9
+ include ThorHelper
10
+ include CollinsShell::Util
11
+ namespace :state
12
+
13
+ def self.banner task, namespace = true, subcommand = false
14
+ "#{basename} #{task.formatted_usage(self, true, subcommand).gsub(':',' ')}"
15
+ end
16
+
17
+ desc 'create', 'Create a new state'
18
+ use_collins_options
19
+ method_option :name, :type => :string, :required => true, :desc => 'Name of state. Must be all caps and unique. Can contain numbers, letters and underscores.'
20
+ method_option :label, :type => :string, :required => true, :desc => 'A friendly (short) description of the state to use in visual labels. Usually just a camel case version of the name, possibly with spaces.'
21
+ method_option :description, :type => :string, :required => true, :desc => 'A longer description of the state'
22
+ method_option :status, :type => :string, :required => false, :desc => 'Optional status to bind the state to'
23
+ def create
24
+ name = options.name.upcase
25
+ label = options.label
26
+ desc = options.description
27
+ status = options.status
28
+ call_collins get_collins_client, "state_create!" do |client|
29
+ if client.state_create!(name, label, desc, status) then
30
+ say_success "Successfully created state '#{name}'"
31
+ else
32
+ say_error "Failed creating state '#{name}'"
33
+ end
34
+ end
35
+ end
36
+
37
+ desc 'delete STATE', 'Delete a state'
38
+ use_collins_options
39
+ def delete state
40
+ call_collins get_collins_client, "state_delete!" do |client|
41
+ if client.state_delete!(state) then
42
+ say_success "Successfully deleted state '#{state}'"
43
+ else
44
+ say_error "Failed deleting state '#{state}'"
45
+ end
46
+ end
47
+ end
48
+
49
+ desc 'get STATE', 'Get a state by name'
50
+ use_collins_options
51
+ def get state
52
+ call_collins get_collins_client, "state_get" do |client|
53
+ header = [["Name", "Label", "Status", "Description"]]
54
+ state = client.state_get(state)
55
+ table = header + [[state.name, state.label, (state.status.name || ""), state.description]]
56
+ print_table table
57
+ end
58
+ end
59
+
60
+ desc 'list', 'list states that are available'
61
+ use_collins_options
62
+ def list
63
+ call_collins get_collins_client, "state_get_all" do |client|
64
+ header = [["Name", "Label", "Status", "Description"]]
65
+ states = header + client.state_get_all.map do |state|
66
+ [state.name, state.label, (state.status.name || ""), state.description]
67
+ end
68
+ print_table states
69
+ end
70
+ end
71
+
72
+ desc 'update STATE', 'Update the name, label, description or status binding of a state'
73
+ use_collins_options
74
+ method_option :name, :type => :string, :required => false, :desc => 'New name of state. Must be all caps and unique. Can contains numbers, letters, and underscores.'
75
+ method_option :label, :type => :string, :required => false, :desc => 'A friendly (short) description of the state to use in visual labels. Usually just a camel case version of the name, possibly with spaces.'
76
+ method_option :description, :type => :string, :required => false, :desc => 'A longer description of the state'
77
+ method_option :status, :type => :string, :required => false, :desc => 'Optional status to bind the state to'
78
+ def update state
79
+ name = options.name.upcase if options.name?
80
+ label = options.label if options.label?
81
+ desc = options.description if options.description?
82
+ status = options.status if options.status?
83
+ call_collins get_collins_client, "state_update!" do |client|
84
+ opts = {
85
+ :name => name, :label => label, :status => status,
86
+ :description => desc
87
+ }
88
+ if client.state_update!(state, opts) then
89
+ say_success "Successfully updated state '#{state}'"
90
+ else
91
+ say_error "Failed creating state '#{state}'"
92
+ end
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,41 @@
1
+ require 'collins_shell/thor'
2
+ require 'collins_shell/util'
3
+ require 'thor'
4
+ require 'thor/group'
5
+
6
+ module CollinsShell
7
+
8
+ class Tag < Thor
9
+
10
+ include ThorHelper
11
+ include CollinsShell::Util
12
+ namespace :tag
13
+
14
+ def self.banner task, namespace = true, subcommand = false
15
+ "#{basename} #{task.formatted_usage(self, true, subcommand).gsub(':',' ')}"
16
+ end
17
+
18
+ desc 'list', 'list tags that are in use'
19
+ use_collins_options
20
+ def list
21
+ call_collins get_collins_client, "list" do |client|
22
+ header = [["Name", "Label", "Description"]]
23
+ tags = header + client.get_all_tags.map do |tag|
24
+ [tag.name, tag.label, tag.description]
25
+ end
26
+ print_table tags
27
+ end
28
+ end
29
+
30
+ desc 'values TAG', 'list all values for the specified tag'
31
+ use_collins_options
32
+ def values tag
33
+ call_collins get_collins_client, "values" do |client|
34
+ client.get_tag_values(tag).sort.each do |value|
35
+ puts(value)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,209 @@
1
+ require 'collins_client'
2
+ require 'highline'
3
+ require 'yaml'
4
+
5
+ module CollinsShell
6
+ module ThorHelper
7
+
8
+ include Collins::Util
9
+
10
+ COLLINS_OPTIONS = {
11
+ :config => {:type => :string, :desc => 'YAML configuration file'},
12
+ :debug => {:type => :boolean, :default => false, :desc => 'Debug output'},
13
+ :host => {:type => :string, :desc => 'Collins host (e.g. http://host:port)'},
14
+ :password => {:type => :string, :desc => 'Collins password'},
15
+ :quiet => {:type => :boolean, :default => false, :desc => 'Be quiet when appropriate'},
16
+ :timeout => {:type => :numeric, :default => 30, :desc => 'Collins client timeout'},
17
+ :username => {:type => :string, :desc => 'Collins username'}
18
+ }
19
+
20
+ PAGE_OPTIONS = {
21
+ :page => {:type => :numeric, :default => 0, :desc => 'Page of results set.'},
22
+ :size => {:type => :numeric, :default => 20, :desc => 'Number of results to return.'},
23
+ :sort => {:type => :string, :default => 'DESC', :desc => 'Sort direction. ASC or DESC'}
24
+ }
25
+
26
+ def self.included(base)
27
+ base.extend(ThorHelper)
28
+ end
29
+
30
+ class << self
31
+ attr_accessor :password
32
+ end
33
+
34
+ def use_collins_options
35
+ COLLINS_OPTIONS.each do |name, options|
36
+ method_option name, options
37
+ end
38
+ end
39
+ def use_page_options default_size = 20
40
+ PAGE_OPTIONS.each do |name, options|
41
+ options.update(:default => default_size) if name == :size
42
+ method_option name, options
43
+ end
44
+ end
45
+ def use_tag_option required = false
46
+ method_option :tag, :type => :string, :required => required, :desc => 'Tag for asset'
47
+ end
48
+ def use_selector_option required = false
49
+ method_option :selector, :type => :hash, :required => required, :desc => 'Selector to query collins. Takes the form of --selector=key1:val1 key2:val2 etc'
50
+ method_option :remote, :type => :boolean, :default => false, :desc => 'Search all collins instances, including remote ones'
51
+ end
52
+
53
+ def selector_or_tag
54
+ if options.selector? then
55
+ options.selector
56
+ elsif options.tag? then
57
+ {:tag => options.tag}
58
+ else
59
+ say_error "Either tag or selector must be specified", :exit => true
60
+ end
61
+ end
62
+
63
+ def require_yes message, color = nil, should_exit = true
64
+ highline = HighLine.new
65
+ colored_message = set_color(message, color)
66
+ answer = ask(colored_message)
67
+ if answer.downcase.strip !~ /^yes$/ then
68
+ if should_exit then
69
+ exit(0)
70
+ else
71
+ false
72
+ end
73
+ else
74
+ true
75
+ end
76
+ end
77
+
78
+ def batch_selector_operation options = {}, &block
79
+ confirmation_message = options[:confirmation_message]
80
+ success_message = options[:success_message]
81
+ error_message = options[:error_message]
82
+ operation = options[:operation]
83
+ require_non_empty(confirmation_message, "confirmation_message option not set")
84
+ require_non_empty(success_message, "success_message not set")
85
+ require_non_empty(error_message, "error_message not set")
86
+ require_non_empty(operation, "operation not set")
87
+ selector = get_selector selector_or_tag, [], nil, options[:remote]
88
+ call_collins get_collins_client, operation do |client|
89
+ assets = client.find selector
90
+ if assets.length > 1 then
91
+ require_yes confirmation_message.call(assets), :red
92
+ end
93
+ assets.each do |asset|
94
+ if block.call(client, asset) then
95
+ say_success success_message.call(asset)
96
+ else
97
+ say_error error_message.call(asset)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def say_error message, options = {}
104
+ if options[:exception] then
105
+ say_status("error", "#{message} - #{options[:exception]}", :red)
106
+ if options[:debug] && options[:debug] then
107
+ pp options[:exception].backtrace
108
+ end
109
+ else
110
+ say_status("error", message, :red)
111
+ end
112
+ if options[:exit].is_a?(TrueClass) then
113
+ exit(1)
114
+ elsif not options[:exit].nil? then
115
+ exit(options[:exit])
116
+ end
117
+ end
118
+
119
+ def say_success message
120
+ say_status("success", message, :green)
121
+ end
122
+
123
+ def get_collins_client opts = {}
124
+ config = get_collins_config.merge(opts).merge(:strict => true)
125
+ config = ensure_password opts
126
+ require_valid_collins_config config
127
+ config[:logger] = get_logger :trace => options.debug, :progname => 'collins-shell'
128
+ Collins::Client.new config
129
+ end
130
+
131
+ def ensure_password opts = {}
132
+ me = CollinsShell::ThorHelper
133
+ if me.password.nil? then
134
+ me.password = get_password(get_collins_config.merge(opts).merge(:strict => true))
135
+ end
136
+ me.password
137
+ end
138
+
139
+ def get_password config
140
+ if
141
+ config[:password] and not
142
+ config[:password].empty? and
143
+ config[:password] != "password" then
144
+ return config
145
+ end
146
+ highline = HighLine.new
147
+ password = highline.ask("Enter your password: ") { |q| q.echo = "x" }
148
+ config.update(:password => password)
149
+ end
150
+
151
+ # --username --password --host is highest priority
152
+ # --config= second highest
153
+ # ~/.collins.yaml is lowest
154
+ def get_collins_config
155
+ def try_config_merge filename, config
156
+ file_config = collins_config_from_file filename
157
+ if file_config[:collins] then
158
+ file_config = file_config[:collins]
159
+ end
160
+ config.update(:host => file_config[:host]) unless options.host?
161
+ config.update(:username => file_config[:username]) unless options.username?
162
+ config.update(:password => file_config[:password]) unless options.password?
163
+ if options.timeout == 30 and file_config[:timeout] then
164
+ config.update(:timeout => file_config[:timeout].to_i)
165
+ end
166
+ end
167
+ config = Hash[
168
+ :host => options.host,
169
+ :username => options.username,
170
+ :password => options.password,
171
+ :timeout => options.timeout
172
+ ]
173
+ if ENV['COLLINS'] then
174
+ try_config_merge ENV['COLLINS'], config
175
+ end
176
+ if options.config? then
177
+ try_config_merge options.config, config
178
+ end
179
+ if File.exists?(File.expand_path("~/.collins.yaml")) then
180
+ user_config = collins_config_from_file "~/.collins.yaml"
181
+ if user_config[:collins] then
182
+ user_config = user_config[:collins]
183
+ end
184
+ config.update(:host => user_config[:host]) if config[:host].nil?
185
+ config.update(:username => user_config[:username]) if config[:username].nil?
186
+ config.update(:password => user_config[:password]) if config[:password].nil?
187
+ if config[:timeout] == 30 and user_config[:timeout] then
188
+ config.update(:timeout => user_config[:timeout].to_i)
189
+ end
190
+ end
191
+ config
192
+ end
193
+
194
+ def collins_config_from_file file
195
+ symbolize_hash(YAML::load(File.open(File.expand_path(file))))
196
+ end
197
+
198
+ def require_valid_collins_config config
199
+ begin
200
+ require_non_empty(config[:host], "collins.host is required")
201
+ require_non_empty(config[:username], "collins.username is required")
202
+ require_non_empty(config[:password], "collins.password is required")
203
+ rescue Exception => e
204
+ raise CollinsShell::ConfigurationError.new(e.message)
205
+ end
206
+ end
207
+
208
+ end
209
+ end