collins_shell 0.2.14

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.
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