hammer_cli 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/LICENSE +5 -0
  2. data/README.md +105 -0
  3. data/bin/hammer +53 -0
  4. data/config/cli_config.template.yml +14 -0
  5. data/doc/design.png +0 -0
  6. data/doc/design.uml +24 -0
  7. data/hammer_cli_complete +13 -0
  8. data/lib/hammer_cli/abstract.rb +95 -0
  9. data/lib/hammer_cli/apipie/command.rb +75 -0
  10. data/lib/hammer_cli/apipie/options.rb +97 -0
  11. data/lib/hammer_cli/apipie/read_command.rb +58 -0
  12. data/lib/hammer_cli/apipie/resource.rb +54 -0
  13. data/lib/hammer_cli/apipie/write_command.rb +35 -0
  14. data/lib/hammer_cli/apipie.rb +3 -0
  15. data/lib/hammer_cli/autocompletion.rb +46 -0
  16. data/lib/hammer_cli/exception_handler.rb +69 -0
  17. data/lib/hammer_cli/exit_codes.rb +23 -0
  18. data/lib/hammer_cli/logger.rb +50 -0
  19. data/lib/hammer_cli/logger_watch.rb +14 -0
  20. data/lib/hammer_cli/main.rb +53 -0
  21. data/lib/hammer_cli/messages.rb +55 -0
  22. data/lib/hammer_cli/option_formatters.rb +13 -0
  23. data/lib/hammer_cli/output/adapter/abstract.rb +27 -0
  24. data/lib/hammer_cli/output/adapter/base.rb +143 -0
  25. data/lib/hammer_cli/output/adapter/silent.rb +17 -0
  26. data/lib/hammer_cli/output/adapter/table.rb +53 -0
  27. data/lib/hammer_cli/output/adapter.rb +6 -0
  28. data/lib/hammer_cli/output/definition.rb +17 -0
  29. data/lib/hammer_cli/output/dsl.rb +56 -0
  30. data/lib/hammer_cli/output/fields.rb +128 -0
  31. data/lib/hammer_cli/output/output.rb +27 -0
  32. data/lib/hammer_cli/output.rb +6 -0
  33. data/lib/hammer_cli/settings.rb +48 -0
  34. data/lib/hammer_cli/shell.rb +49 -0
  35. data/lib/hammer_cli/validator.rb +114 -0
  36. data/lib/hammer_cli/version.rb +5 -0
  37. data/lib/hammer_cli.rb +13 -0
  38. metadata +189 -0
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ This program and entire repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.
2
+
3
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
4
+
5
+ You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ Hammer - the CLI tool for foreman
2
+ =================================
3
+
4
+ ![Design draft](doc/design.png)
5
+
6
+ As the diagram shows, the CLI consist of almost generic framework (shell-like environment, autocompletion, command help text, option evaluation and command invocation) and set of plugins defining the actual commands. This setup is flexible and allows us to easily install different sets of commands for different products. The plugins are independant and can implement any action as an command, so that besides commands calling Foreman API you can have commands calling varius admin tasks, etc.
7
+
8
+
9
+ The CLI has
10
+
11
+ - Git-like subcomands
12
+ - system shell autocompletion for commands and options
13
+ - shell-like environment with autocompletion and history where the commands can be run directly
14
+ - commands extensible via plugins
15
+ - been implemented in Ruby
16
+
17
+
18
+ If you are interested you can help us by sending patches or filing bugs and feature requests (there is CLI catgory in Redmine)
19
+
20
+
21
+ How to run
22
+ ----------
23
+
24
+ The work is in progress and there are still no builds ready, but instaling from sources is easy. You will need rake, bundler.
25
+ Clone and install CLI core
26
+
27
+ $ git clone git@github.com:theforeman/hammer-cli.git
28
+ $ cd hammer-cli
29
+ $ rake install
30
+ $ cd ..
31
+
32
+
33
+ clone plugin with foreman commands
34
+
35
+ $ git clone git@github.com:theforeman/hammer-cli-foreman.git
36
+ $ cd hammer-cli-foreman
37
+ $ rake install
38
+ $ cd ..
39
+
40
+ and configure. Configuration is by default looked for in ~/.foreman/ or in /etc/foreman/.
41
+ Optionally you can put your configuration in ./config/ or point hammer
42
+ to some other location using -c CONF_FILE option
43
+
44
+ You can start with config file template we created for you and update it to suit your needs. E.g.:
45
+
46
+ $ cp hammer-cli/config/cli_config.template.yaml ~/.foreman/cli_config.yml
47
+
48
+ and run
49
+
50
+ $ hammer -h
51
+ Usage:
52
+ hammer [OPTIONS] SUBCOMMAND [ARG] ...
53
+
54
+ Parameters:
55
+ SUBCOMMAND subcommand
56
+ [ARG] ... subcommand arguments
57
+
58
+ Subcommands:
59
+ shell Interactive Shell
60
+ architecture Manipulate Foreman's architectures.
61
+ compute_resource Manipulate Foreman's architectures.
62
+ domain Manipulate Foreman's domains.
63
+ organization Manipulate Foreman's organizations.
64
+ subnet Manipulate Foreman's subnets.
65
+ user Manipulate Foreman's users.
66
+
67
+ Options:
68
+ -v, --verbose be verbose
69
+ -u, --username USERNAME username to access the remote system
70
+ -p, --password PASSWORD password to access the remote system
71
+ --version show version
72
+ --autocomplete LINE Get list of possible endings
73
+ -h, --help print help
74
+
75
+
76
+ Autocompletion
77
+ --------------
78
+
79
+ It is necessary to copy script hammer_cli_complete to the bash_completion.d directory.
80
+
81
+ $ sudo cp hammer-cli/hammer_cli_complete /etc/bash_completion.d/
82
+
83
+ Then in new shell the completion should work.
84
+
85
+
86
+ How to test
87
+ ------------
88
+
89
+ Development of almost all the code was test driven.
90
+
91
+ $ bundle install
92
+ $ bundle exec "rake test"
93
+
94
+ should work in any of the cli related repos. Generated coverage reports are stored in ./coverage directory.
95
+
96
+ License
97
+ -------
98
+
99
+ This project is licensed under the GPLv3+.
100
+
101
+
102
+ Acknowledgements
103
+ ----------------
104
+
105
+ Thanks to Brian Gupta for the initial work and a great name.
data/bin/hammer ADDED
@@ -0,0 +1,53 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'clamp'
5
+
6
+ # create fake command instance to use some global args before we start
7
+ class PreParser < Clamp::Command
8
+ option ["-v", "--verbose"], :flag, "be verbose"
9
+ option ["-c", "--config"], "CFG_FILE", "path to custom config file"
10
+ end
11
+
12
+ preparser = PreParser.new File.basename($0), {}
13
+ begin
14
+ preparser.parse ARGV
15
+ rescue
16
+ end
17
+
18
+ # load user's settings
19
+ require 'hammer_cli/settings'
20
+
21
+ CFG_PATH = ['./config/cli_config.yml', '~/.foreman/cli_config.yml', '/etc/foreman/cli_config.yml']
22
+
23
+ if preparser.config
24
+ CFG_PATH.unshift preparser.config
25
+ end
26
+
27
+ HammerCLI::Settings.load_from_file CFG_PATH
28
+
29
+ # setup logging
30
+ require 'hammer_cli/logger'
31
+ logger = Logging.logger['Init']
32
+
33
+ if preparser.verbose?
34
+ root_logger = Logging.logger.root
35
+ root_logger.appenders = root_logger.appenders << ::Logging.appenders.stderr(:layout => HammerCLI::Logger::COLOR_LAYOUT)
36
+ end
37
+
38
+ # log which config was loaded (now when we have logging)
39
+ HammerCLI::Settings.path_history.each do |path|
40
+ logger.info "Configuration from the file #{path} has been loaded"
41
+ end
42
+
43
+ # load hammer core
44
+ require 'hammer_cli'
45
+
46
+ # load modules set in config
47
+ modules = HammerCLI::Settings[:modules] || []
48
+ modules.each do |m|
49
+ require m
50
+ logger.info "Extension module #{m} loaded"
51
+ end
52
+
53
+ exit HammerCLI::MainCommand.run || 0
@@ -0,0 +1,14 @@
1
+ :modules:
2
+ # - hammer_cli_foreman
3
+ # - hammer_cli_katello_bridge
4
+
5
+ :host: 'https://localhost/'
6
+ :username: 'admin'
7
+ :password: 'changeme'
8
+
9
+ # :watch_plain: true disables color output of logger.watch in Clamp commands
10
+ :watch_plain: false
11
+
12
+ #:log_dir: '/var/log/foreman'
13
+ :log_dir: '~/.foreman/log'
14
+ :log_level: 'error'
data/doc/design.png ADDED
Binary file
data/doc/design.uml ADDED
@@ -0,0 +1,24 @@
1
+ @startuml
2
+
3
+
4
+ [Foreman API] as FAPI
5
+ FAPI -up-> [Foreman Server]
6
+ [Foreman Commands Plugin] as FCP
7
+ FCP -up-> FAPI
8
+
9
+ [Katello CLI via system call] as KCLI
10
+ KCLI -up-> [Katello Server]
11
+ [Katello Commands Plugin] as KCP
12
+ KCP -up-> KCLI
13
+
14
+ package "CLI Framework" {
15
+ () "Command Plugins" as CP
16
+ [Shell]
17
+ [Autocompletion]
18
+ }
19
+
20
+ CP -up-> [Other Commands Plugin(s)]
21
+ CP -up-> FCP
22
+ CP -up-> KCP
23
+
24
+ @enduml
@@ -0,0 +1,13 @@
1
+ #
2
+ # Hammer CLI bash completion script
3
+ #
4
+ # vim:ts=2:sw=2:et:
5
+ #
6
+
7
+ _hammer() {
8
+ COMPREPLY=($(hammer --autocomplete "${COMP_WORDS[*]}"))
9
+ return 0
10
+ }
11
+
12
+ complete -F _hammer hammer
13
+
@@ -0,0 +1,95 @@
1
+ require 'hammer_cli/autocompletion'
2
+ require 'hammer_cli/exception_handler'
3
+ require 'hammer_cli/logger_watch'
4
+ require 'clamp'
5
+ require 'logging'
6
+
7
+ module HammerCLI
8
+
9
+ class AbstractCommand < Clamp::Command
10
+
11
+ extend Autocompletion
12
+ class << self
13
+ attr_accessor :validation_block
14
+ end
15
+
16
+ def run(arguments)
17
+ exit_code = super(arguments)
18
+ raise "exit code must be integer" unless exit_code.is_a? Integer
19
+ return exit_code
20
+ rescue => e
21
+ # do not catch Clamp errors
22
+ raise if e.class <= Clamp::UsageError || e.class <= Clamp::HelpWanted
23
+ handle_exception e
24
+ end
25
+
26
+ def parse(arguments)
27
+ super(arguments)
28
+ validate_options
29
+ logger.info "Called with options: %s" % options.inspect
30
+ rescue HammerCLI::Validator::ValidationError => e
31
+ signal_usage_error e.message
32
+ end
33
+
34
+ def execute
35
+ HammerCLI::EX_OK
36
+ end
37
+
38
+ def self.validate_options &block
39
+ self.validation_block = block
40
+ end
41
+
42
+ def validate_options
43
+ validator.run &self.class.validation_block if self.class.validation_block
44
+ end
45
+
46
+
47
+ def output
48
+ @output ||= HammerCLI::Output::Output.new
49
+ end
50
+
51
+ def exception_handler
52
+ @exception_handler ||= exception_handler_class.new :output => output
53
+ end
54
+
55
+ protected
56
+
57
+ def logger name=self.class
58
+ logger = Logging.logger[name]
59
+ logger.extend(HammerCLI::Logger::Watch) if not logger.respond_to? :watch
60
+ logger
61
+ end
62
+
63
+ def validator
64
+ options = self.class.recognised_options.collect{|opt| opt.of(self)}
65
+ @validator ||= HammerCLI::Validator.new(options)
66
+ end
67
+
68
+ def handle_exception e
69
+ exception_handler.handle_exception(e)
70
+ end
71
+
72
+ def exception_handler_class
73
+ #search for exception handler class in parent modules/classes
74
+ module_list = self.class.name.to_s.split('::').inject([Object]) do |mod, class_name|
75
+ mod << mod[-1].const_get(class_name)
76
+ end
77
+ module_list.reverse.each do |mod|
78
+ return mod.send(:exception_handler_class) if mod.respond_to? :exception_handler_class
79
+ end
80
+ return HammerCLI::ExceptionHandler
81
+ end
82
+
83
+ def all_options
84
+ self.class.recognised_options.inject({}) do |h, opt|
85
+ h[opt.attribute_name] = send(opt.read_method)
86
+ h
87
+ end
88
+ end
89
+
90
+ def options
91
+ all_options.reject {|key, value| value.nil? }
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,75 @@
1
+ require File.join(File.dirname(__FILE__), 'options')
2
+ require File.join(File.dirname(__FILE__), 'resource')
3
+
4
+ module HammerCLI::Apipie
5
+
6
+ class Command < HammerCLI::AbstractCommand
7
+
8
+ include HammerCLI::Apipie::Resource
9
+ include HammerCLI::Apipie::Options
10
+
11
+ def initialize(*args)
12
+ setup_identifier_options
13
+ super(*args)
14
+ end
15
+
16
+ def setup_identifier_options
17
+ self.class.identifier_option(:id, "resource id")
18
+ self.class.identifier_option(:name, "resource name")
19
+ self.class.identifier_option(:label, "resource label")
20
+ end
21
+
22
+ def self.identifiers *keys
23
+ @identifiers ||= {}
24
+ keys.each do |key|
25
+ if key.is_a? Hash
26
+ @identifiers.merge!(key)
27
+ else
28
+ @identifiers.update(key => key)
29
+ end
30
+ end
31
+ end
32
+
33
+ def validate_options
34
+ super
35
+ validator.any(*self.class.declared_identifiers.values).required
36
+ end
37
+
38
+ protected
39
+
40
+ def get_identifier
41
+ self.class.declared_identifiers.keys.each do |identifier|
42
+ value = find_option("--"+identifier.to_s).of(self).read
43
+ return [value, identifier] if value
44
+ end
45
+ [nil, nil]
46
+ end
47
+
48
+ def self.identifier? key
49
+ if @identifiers
50
+ return true if @identifiers.keys.include? key
51
+ else
52
+ return true if superclass.respond_to?(:identifier?, true) and superclass.identifier?(key)
53
+ end
54
+ return false
55
+ end
56
+
57
+ def self.declared_identifiers
58
+ if @identifiers
59
+ return @identifiers
60
+ elsif superclass.respond_to?(:declared_identifiers, true)
61
+ superclass.declared_identifiers
62
+ else
63
+ {}
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def self.identifier_option(name, desc)
70
+ attr_name = declared_identifiers[name]
71
+ option "--"+name.to_s, name.to_s.upcase, desc, :attribute_name => attr_name if self.identifier? name
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,97 @@
1
+ require 'hammer_cli/option_formatters'
2
+
3
+ module HammerCLI::Apipie
4
+ module Options
5
+
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ def all_method_options
11
+ method_options_for_params(self.class.method_doc["params"], true)
12
+ end
13
+
14
+ def method_options
15
+ method_options_for_params(self.class.method_doc["params"], false)
16
+ end
17
+
18
+ def method_options_for_params params, include_nil=true
19
+ opts = {}
20
+ params.each do |p|
21
+ if p["expected_type"] == "hash"
22
+ opts[p["name"]] = method_options_for_params(p["params"], include_nil)
23
+ elsif respond_to?(p["name"], true)
24
+ opts[p["name"]] = send(p["name"])
25
+ else
26
+ opts[p["name"]] = nil
27
+ end
28
+ end
29
+ opts.reject! {|key, value| value.nil? } unless include_nil
30
+ opts
31
+ end
32
+
33
+ module ClassMethods
34
+
35
+ def apipie_options options={}
36
+ raise "Specify apipie resource first." unless resource_defined?
37
+
38
+ filter = options[:without] || []
39
+ filter = Array(filter)
40
+
41
+ options_for_params(method_doc["params"], filter)
42
+ end
43
+
44
+ protected
45
+
46
+ def options_for_params params, filter
47
+ params.each do |p|
48
+ next if filter.include? p["name"].to_s or filter.include? p["name"].to_sym
49
+ if p["expected_type"] == "hash"
50
+ options_for_params(p["params"], filter)
51
+ else
52
+ create_option p
53
+ end
54
+ end
55
+ end
56
+
57
+ def create_option param
58
+ option(
59
+ option_switches(param),
60
+ option_type(param),
61
+ option_desc(param),
62
+ option_opts(param),
63
+ &option_formatter(param)
64
+ )
65
+ end
66
+
67
+ def option_switches param
68
+ '--' + param["name"].gsub('_', '-')
69
+ end
70
+
71
+ def option_type param
72
+ param["name"].upcase.gsub('-', '_')
73
+ end
74
+
75
+ def option_desc param
76
+ desc = param["description"].gsub(/<\/?[^>]+?>/, "")
77
+ return " " if desc.empty?
78
+ return desc
79
+ end
80
+
81
+ def option_opts param
82
+ opts = {}
83
+ opts[:required] = true if param["required"]
84
+ return opts
85
+ end
86
+
87
+ def option_formatter param
88
+ # FIXME: There is a bug in apipie, it does not produce correct expected type for Arrays
89
+ # When it's fixed, we should test param["expected_type"] == "array"
90
+ if param["validator"].include? "Array"
91
+ HammerCLI::OptionFormatters.method(:list)
92
+ end
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,58 @@
1
+ require 'hammer_cli/output/dsl'
2
+
3
+ module HammerCLI::Apipie
4
+
5
+ class ReadCommand < Command
6
+
7
+ def self.output definition=nil, &block
8
+ dsl = HammerCLI::Output::Dsl.new
9
+ dsl.build &block
10
+
11
+ output_definition.append definition.fields unless definition.nil?
12
+ output_definition.append dsl.fields
13
+ end
14
+
15
+ def self.heading heading=nil
16
+ @heading = heading if heading
17
+ @heading
18
+ end
19
+
20
+ def output_definition
21
+ self.class.output_definition
22
+ end
23
+
24
+ def self.output_definition
25
+ @output_definition ||= HammerCLI::Output::Definition.new
26
+ @output_definition
27
+ end
28
+
29
+ def output
30
+ @output ||= HammerCLI::Output::Output.new :definition => output_definition
31
+ end
32
+
33
+ def execute
34
+ d = retrieve_data
35
+ logger.watch "Retrieved data: ", d
36
+ print_data d
37
+ return HammerCLI::EX_OK
38
+ end
39
+
40
+ protected
41
+ def retrieve_data
42
+ raise "resource or action not defined" unless self.class.resource_defined?
43
+ resource.send(action, request_params)[0]
44
+ end
45
+
46
+ def print_data(records)
47
+ output.print_records(records, self.class.heading)
48
+ end
49
+
50
+ def request_params
51
+ method_options
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+
58
+
@@ -0,0 +1,54 @@
1
+ module HammerCLI::Apipie
2
+ module Resource
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ def resource
9
+ @resource ||= self.class.resource.new resource_config
10
+ @resource
11
+ end
12
+
13
+ def action
14
+ self.class.action
15
+ end
16
+
17
+ def resource_config
18
+ config = {}
19
+ config[:base_url] = HammerCLI::Settings[:host]
20
+ config[:username] = context[:username] || HammerCLI::Settings[:username] || ENV['FOREMAN_USERNAME']
21
+ config[:password] = context[:password] || HammerCLI::Settings[:password] || ENV['FOREMAN_PASSWORD']
22
+ config
23
+ end
24
+
25
+ module ClassMethods
26
+
27
+
28
+ def resource resource=nil, action=nil
29
+ @api_resource = resource unless resource.nil?
30
+ @api_action = action unless action.nil?
31
+ return @api_resource if @api_resource
32
+ return superclass.resource
33
+ end
34
+
35
+ def action action=nil
36
+ @api_action = action unless action.nil?
37
+ @api_action
38
+ end
39
+
40
+ def method_doc
41
+ @api_resource.doc["methods"].each do |method|
42
+ return method if method["name"] == @api_action.to_s
43
+ end
44
+ raise "No method documentation found for #{@api_resource}##{@api_action}"
45
+ end
46
+
47
+ def resource_defined?
48
+ not (@api_resource.nil? or @api_action.nil?)
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ require 'hammer_cli/messages'
2
+
3
+ module HammerCLI::Apipie
4
+
5
+ class WriteCommand < Command
6
+
7
+ include HammerCLI::Messages
8
+
9
+ def execute
10
+ send_request
11
+ print_message
12
+ return HammerCLI::EX_OK
13
+ end
14
+
15
+ protected
16
+
17
+ def print_message
18
+ msg = success_message
19
+ output.print_message msg unless msg.nil?
20
+ end
21
+
22
+ def send_request
23
+ raise "resource or action not defined" unless self.class.resource_defined?
24
+ resource.send(action, request_params)[0]
25
+ end
26
+
27
+ def request_params
28
+ method_options
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), './apipie/command')
2
+ require File.join(File.dirname(__FILE__), './apipie/read_command')
3
+ require File.join(File.dirname(__FILE__), './apipie/write_command')
@@ -0,0 +1,46 @@
1
+ module HammerCLI
2
+ module Autocompletion
3
+
4
+ def autocomplete(line, prefix=[])
5
+ endings = []
6
+ formated_prefix = prefix.join(' ')
7
+
8
+ if line.length == 0 # look for possible next words
9
+ all_options = collect_all_options
10
+ endings = all_options.keys.map { |e| [e, formated_prefix] }
11
+ elsif line.length == 1 && !(find_subcommand(line[0]) || find_option(line[0])) # look for endings
12
+ all_options = collect_all_options
13
+ endings = all_options.select { |k,v| k if k.start_with? line[0] }.keys.map { |e| [e, formated_prefix] }
14
+ else # dive into subcommands
15
+ subcommand = find_subcommand line[0]
16
+ if subcommand
17
+ command = line.shift
18
+ prefix << command
19
+ endings = subcommand.subcommand_class.autocomplete(line, prefix)
20
+ end
21
+ end
22
+ endings
23
+ end
24
+
25
+ def collect_all_options
26
+ all_options = {}
27
+
28
+ if has_subcommands?
29
+ recognised_subcommands.each do |item|
30
+
31
+ label, _ = item.help
32
+ all_options[label] = item
33
+ end
34
+ end
35
+
36
+ recognised_options.each do |item|
37
+ label, _ = item.help
38
+ label.split(',').each do |option|
39
+ all_options[option.split[0]] = item
40
+ end
41
+ end
42
+
43
+ all_options
44
+ end
45
+ end
46
+ end