razor-client 1.1.0 → 1.2.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.
data/NEWS.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Razor Client Release Notes
2
2
 
3
+ ## 1.2.0 - 2016-03-08
4
+
5
+ * BUGFIX: Razor client version will be reported even if the Razor server is
6
+ unreachable.
7
+ * BUGFIX: Fixed insecure flag when supplied in addition to a server URL.
8
+ * NEW: Added positional argument support, when supplied by the server. See
9
+ `razor <command> --help` for details on usage.
10
+ * NEW: Added USAGE section to the command's help, which will include positional
11
+ arguments, if any exist.
12
+ * IMPROVEMENT: Proper short form argument style is now followed.
13
+ Single-character arguments now require a single dash, e.g. `-c`.
14
+ * IMPROVEMENT: Error messaging for SSL issues has been improved.
15
+
3
16
  ## 1.1.0 - 2015-11-12
4
17
 
5
18
  * IMPROVEMENT: By default, `razor` will point to port 8150.
data/bin/razor CHANGED
@@ -28,8 +28,9 @@ rescue OptionParser::InvalidOption => e
28
28
  end
29
29
 
30
30
  if parse.show_version?
31
- puts parse.version
32
- exit 0
31
+ version, exit_code = parse.version
32
+ puts version
33
+ exit exit_code
33
34
  end
34
35
 
35
36
  if parse.show_help? and not parse.show_command_help?
@@ -1,54 +1,60 @@
1
1
  class Razor::CLI::Command
2
- def initialize(parse, navigate, commands, segments)
3
- @parse = parse
2
+ def initialize(parse, navigate, command, segments, cmd_url)
3
+ @dump_response = parse && parse.dump_response?
4
+ @show_command_help = parse && parse.show_command_help?
4
5
  @navigate = navigate
5
- @commands = commands
6
+ @command = command
7
+ @cmd_schema = command ? command['schema'] : nil
8
+ @cmd_url = cmd_url
6
9
  @segments = segments
7
10
  end
8
11
 
9
12
  def run
10
- # @todo lutter 2013-08-16: None of this has any tests, and error
11
- # handling is heinous at best
12
- cmd, body = extract_command
13
- # Ensure that we copy authentication data from our previous URL.
14
- url = URI.parse(cmd["id"])
15
- if @doc_resource
16
- url = URI.parse(url.to_s)
17
- end
18
-
19
- if @parse.show_command_help?
20
- @navigate.json_get(url)
13
+ body = extract_command
14
+ if @show_command_help
15
+ @command
21
16
  else
22
17
  if body.empty?
23
18
  raise Razor::CLI::Error,
24
19
  "No arguments for command (did you forget --json ?)"
25
20
  end
26
- result = @navigate.json_post(url, body)
21
+ result = @navigate.json_post(@cmd_url, body)
27
22
  # Get actual object from the id.
28
23
  result = result.merge(@navigate.json_get(URI.parse(result['id']))) if result['id']
29
24
  result
30
25
  end
31
26
  end
32
27
 
33
- def command(name)
34
- @command ||= @commands.find { |coll| coll["name"] == name }
35
- end
36
-
37
28
  def extract_command
38
- cmd = command(@segments.shift)
39
- @cmd_url = URI.parse(cmd['id'])
40
- @cmd_schema = cmd_schema(@cmd_url)
41
29
  body = {}
30
+ pos_index = 0
42
31
  until @segments.empty?
43
32
  argument = @segments.shift
44
- if argument =~ /\A--([a-z-]+)(=(.+))?\Z/
45
- # `--arg=value` or `--arg value`
46
- arg, value = [$1, $3]
33
+ if argument =~ /\A--([a-z-]{2,})(=(.+))?\Z/ or
34
+ argument =~ /\A-([a-z])(=(.+))?\Z/
35
+ # `--arg=value`/`--arg value`
36
+ # `-a=value`/`-a value`
37
+ arg_name, value = [$1, $3]
47
38
  value = @segments.shift if value.nil? && @segments[0] !~ /^--/
48
- arg = self.class.resolve_alias(arg, @cmd_schema)
49
- body[arg] = self.class.convert_arg(arg, value, body[arg], @cmd_schema)
39
+ arg_name = self.class.resolve_alias(arg_name, @cmd_schema)
40
+ body[arg_name] = self.class.convert_arg(arg_name, value, body[arg_name], @cmd_schema)
41
+ elsif argument =~ /\A-([a-z-]{2,})(=(.+))?\Z/ and
42
+ @cmd_schema[self.class.resolve_alias($1, @cmd_schema)]
43
+ # Short form, should be long; offer suggestion
44
+ raise ArgumentError, "Unexpected argument #{argument} (did you mean --#{$1}?)"
45
+ elsif argument =~ /\A--([a-z])(=(.+))?\Z/ and
46
+ @cmd_schema[self.class.resolve_alias($1, @cmd_schema)]
47
+ # Long form, should be short; offer suggestion
48
+ raise ArgumentError, "Unexpected argument #{argument} (did you mean -#{$1}?)"
50
49
  else
51
- raise ArgumentError, "Unexpected argument #{argument}"
50
+ # This may be a positional argument.
51
+ arg_name = positional_argument(@cmd_schema, pos_index)
52
+ if arg_name
53
+ body[arg_name] = self.class.convert_arg(arg_name, argument, body[arg_name], @cmd_schema)
54
+ pos_index += 1
55
+ else
56
+ raise ArgumentError, "Unexpected argument #{argument}"
57
+ end
52
58
  end
53
59
  end
54
60
 
@@ -62,15 +68,14 @@ class Razor::CLI::Command
62
68
  raise Razor::CLI::Error,
63
69
  "Permission to read file #{body["json"]} denied"
64
70
  end
65
- [cmd, body]
71
+ body
66
72
  end
67
73
 
68
- def cmd_schema(cmd_url)
69
- begin
70
- @navigate.json_get(cmd_url)['schema']
71
- rescue RestClient::ResourceNotFound => _
72
- raise VersionCompatibilityError, 'Server must supply the expected datatypes for command arguments; use `--json` or upgrade razor-server'
73
- end
74
+ def positional_argument(cmd_schema, pos_index)
75
+ # Find a matching position and return its argument name.
76
+ cmd_schema && cmd_schema.select do |_, schema|
77
+ schema['position'] == pos_index
78
+ end.keys.first
74
79
  end
75
80
 
76
81
  def self.arg_type(arg_name, cmd_schema)
@@ -94,7 +99,7 @@ class Razor::CLI::Command
94
99
  argument_type = arg_type(arg_name, cmd_schema)
95
100
 
96
101
  # This might be helpful, since there's no other method for debug-level logging on the client.
97
- puts "Formatting argument #{arg_name} with value #{value} as #{argument_type}\n" if @parse && @parse.dump_response?
102
+ puts "Formatting argument #{arg_name} with value #{value} as #{argument_type}\n" if @dump_response
98
103
 
99
104
  case argument_type
100
105
  when "array"
@@ -65,33 +65,48 @@ module Razor::CLI
65
65
  def format_command_help(doc, show_api_help)
66
66
  item = doc.items.first
67
67
  raise Razor::CLI::Error, 'Could not find help for that entry' unless item.has_key?('help')
68
- if show_api_help and (item['help'].has_key?('summary') or item['help'].has_key?('description'))
69
- format_composed_help(item['help']).chomp
70
- elsif item['help'].has_key?('summary') or item['help'].has_key?('description')
71
- format_composed_help(item['help'], item['help']['examples']['cli']).chomp
68
+ if item['help'].has_key?('examples')
69
+ if show_api_help && item['help']['examples'].has_key?('api')
70
+ format_composed_help(item, item['help']['examples']['api']).chomp
71
+ else
72
+ format_composed_help(item).chomp
73
+ end
72
74
  else
73
75
  format_full_help(item['help']).chomp
74
76
  end
75
77
  end
76
78
 
77
- def format_composed_help(object, examples = object['examples']['api'])
79
+ def positional_args_usage(object)
80
+ object['schema'].map do |k, v|
81
+ [v['position'], k] if v.has_key?('position')
82
+ end.compact.sort.map(&:last).
83
+ map {|attr| "[#{attr.gsub('_', '-')}] " }.join.strip
84
+ end
85
+ def format_composed_help(object, examples = object['help']['examples']['cli'])
86
+ help_obj = object['help']
78
87
  ret = ''
79
- ret = ret + <<-SYNOPSIS if object.has_key?('summary')
88
+ ret = ret + <<-USAGE
89
+ # USAGE
90
+
91
+ razor #{object['name']} #{positional_args_usage(object)} <flags>
92
+
93
+ USAGE
94
+ ret = ret + <<-SYNOPSIS if help_obj.has_key?('summary')
80
95
  # SYNOPSIS
81
- #{object['summary']}
96
+ #{help_obj['summary']}
82
97
 
83
98
  SYNOPSIS
84
- ret = ret + <<-DESCRIPTION if object.has_key?('description')
99
+ ret = ret + <<-DESCRIPTION if help_obj.has_key?('description')
85
100
  # DESCRIPTION
86
- #{object['description']}
101
+ #{help_obj['description']}
87
102
 
88
- #{object['schema']}
103
+ #{help_obj['schema']}
89
104
  DESCRIPTION
90
- ret = ret + <<-RETURNS if object.has_key?('returns')
105
+ ret = ret + <<-RETURNS if help_obj.has_key?('returns')
91
106
  # RETURNS
92
- #{object['returns'].gsub(/^/, ' ')}
107
+ #{help_obj['returns'].gsub(/^/, ' ')}
93
108
  RETURNS
94
- ret = ret + <<-EXAMPLES if object.has_key?('examples') && object['examples'].has_key?('cli')
109
+ ret = ret + <<-EXAMPLES if examples
95
110
  # EXAMPLES
96
111
 
97
112
  #{examples.gsub(/^/, ' ')}
@@ -93,7 +93,15 @@ module Razor::CLI
93
93
  elsif query?
94
94
  Razor::CLI::Query.new(@parse, self, collections, @segments).run
95
95
  elsif command?
96
- Razor::CLI::Command.new(@parse, self, commands, @segments).run
96
+ cmd = @segments.shift
97
+ command = commands.find { |coll| coll["name"] == cmd }
98
+ cmd_url = URI.parse(command['id'])
99
+ # Ensure that we copy authentication data from our previous URL.
100
+ if @doc_resource
101
+ cmd_url = URI.parse(cmd_url.to_s)
102
+ end
103
+ command = json_get(cmd_url)
104
+ Razor::CLI::Command.new(@parse, self, command, @segments, cmd_url).run
97
105
  else
98
106
  raise NavigationError.new(@doc_resource, @segments, @doc)
99
107
  end
@@ -63,22 +63,19 @@ module Razor::CLI
63
63
  end
64
64
 
65
65
  def version
66
+ server_version = '(unknown)'
67
+ error = ''
66
68
  begin
67
- <<-VERSION
68
- Razor Server version: #{navigate.server_version}
69
- Razor Client version: #{Razor::CLI::VERSION}
70
- VERSION
69
+ server_version = navigate.server_version
71
70
  rescue RestClient::Unauthorized
72
- puts <<-UNAUTH
73
- Error: Credentials are required to connect to the server at #{@api_url}"
74
- UNAUTH
75
- exit 1
71
+ error = "Error: Credentials are required to connect to the server at #{@api_url}."
76
72
  rescue
77
- puts <<-ERR
78
- Error: Could not connect to the server at #{@api_url}. More help is available after pointing
79
- the client to a Razor server
80
- ERR
81
- exit 1
73
+ error = "Error: Could not connect to the server at #{@api_url}."
74
+ ensure
75
+ return [(<<-OUTPUT + "\n" + error).rstrip, error != '' ? 1 : 0]
76
+ Razor Server version: #{server_version}
77
+ Razor Client version: #{Razor::CLI::VERSION}
78
+ OUTPUT
82
79
  end
83
80
  end
84
81
 
@@ -105,10 +102,23 @@ HELP
105
102
  Error: Credentials are required to connect to the server at #{@api_url}"
106
103
  UNAUTH
107
104
  exit = 1
108
- rescue
105
+ rescue SocketError, Errno::ECONNREFUSED => e
106
+ puts "Error: Could not connect to the server at #{@api_url}"
107
+ puts " #{e}\n"
108
+ die
109
+ rescue RestClient::SSLCertificateNotVerified
110
+ puts "Error: SSL certificate could not be verified against known CA certificates."
111
+ puts " To turn off verification, use the -k or --insecure option."
112
+ die
113
+ rescue OpenSSL::SSL::SSLError => e
114
+ # Occurs in case of e.g. certificate mismatch (FQDN vs. hostname)
115
+ puts "Error: SSL certificate error from server at #{@api_url}"
116
+ puts " #{e}"
117
+ die
118
+ rescue => e
109
119
  output << <<-ERR
110
- Error: Could not connect to the server at #{@api_url}. More help is available after pointing
111
- the client to a Razor server
120
+ Error: Unknown error occurred while connecting to server at #{@api_url}:
121
+ #{e}
112
122
  ERR
113
123
  exit = 1
114
124
  end
@@ -143,23 +153,34 @@ ERR
143
153
  # The format can be determined from later segments.
144
154
  attr_accessor :format, :stripped_args, :ssl_ca_file
145
155
 
156
+ LINUX_PEM_FILE = '/etc/puppetlabs/puppet/ssl/certs/ca.pem'
157
+ WIN_PEM_FILE = 'C:\ProgramData\PuppetLabs\puppet\etc\ssl\certs\ca.pem'
146
158
  def initialize(args)
147
159
  parse_and_set_api_url(ENV["RAZOR_API"] || DEFAULT_RAZOR_API, :env)
148
160
  @args = args.dup
149
161
  # To be populated externally.
150
162
  @stripped_args = []
151
163
  @format = 'short'
164
+ @verify_ssl = true
165
+ env_pem_file = ENV['RAZOR_CA_FILE']
152
166
  # If this is set, it should actually exist.
153
- if ENV['RAZOR_CA_FILE'] && !File.exists?(ENV['RAZOR_CA_FILE'])
154
- raise Razor::CLI::InvalidCAFileError.new(ENV['RAZOR_CA_FILE'])
167
+ if env_pem_file && !File.exists?(env_pem_file)
168
+ raise Razor::CLI::InvalidCAFileError.new(env_pem_file)
169
+ end
170
+ pem_file_locations = [env_pem_file, LINUX_PEM_FILE, WIN_PEM_FILE]
171
+ pem_file_locations.each do |file|
172
+ if file && File.exists?(file)
173
+ @ssl_ca_file = file
174
+ break
175
+ end
155
176
  end
156
- ca_file = ENV["RAZOR_CA_FILE"]
157
- @ssl_ca_file = ca_file if ca_file && File.exists?(ca_file)
158
177
  @args = get_optparse.order(@args)
159
178
 
160
179
  # Localhost won't match the server's certificate; no verification required.
161
180
  # This needs to happen after get_optparse so `-k` and `-u` can take effect.
162
- @verify_ssl ||= (@api_url.hostname != 'localhost')
181
+ if @api_url.hostname == 'localhost'
182
+ @verify_ssl = false
183
+ end
163
184
 
164
185
  @args = set_help_vars(@args)
165
186
  if @args == ['version'] or @show_version
@@ -54,7 +54,7 @@ class Razor::CLI::TableFormat
54
54
  def average_width(headings)
55
55
  # The 3 here = 2 for width gap + 1 for the column separator.
56
56
  # The 1 is for the last separator.
57
- console_width = `stty size | cut -d ' ' -f 2`
57
+ console_width = `stty size | cut -d ' ' -f 2 2>/dev/null`
58
58
  if console_width.nil? || console_width.to_i <= 0
59
59
  console_width = 80
60
60
  end
@@ -18,7 +18,7 @@ module Razor
18
18
  #
19
19
  # The next line is the one that our packaging tools modify, so please make
20
20
  # sure that any change to it is discussed and agreed first.
21
- version = '1.1.0'
21
+ version = '1.2.0'
22
22
 
23
23
  if version == "DEVELOPMENT"
24
24
  root = File.expand_path("../../..", File.dirname(__FILE__))
@@ -63,4 +63,100 @@ describe Razor::CLI::Command do
63
63
  result.should == 'abc'
64
64
  end
65
65
  end
66
+
67
+ context "extract_command" do
68
+
69
+ def extract(schema, run_array)
70
+ c = Razor::CLI::Command.new(nil, nil, schema, run_array, nil)
71
+ c.extract_command
72
+ end
73
+ context "flag length" do
74
+
75
+ it "fails with a single dash for long flags" do
76
+ expect{extract({'schema' => {'name' => {'type' => 'array'}}}, ['-name', 'abc'])}.
77
+ to raise_error(ArgumentError, 'Unexpected argument -name (did you mean --name?)')
78
+ end
79
+ it "fails with a double dash for short flags" do
80
+ expect{extract({'schema' => {'n' => {'type' => 'array'}}}, ['--n', 'abc'])}.
81
+ to raise_error(ArgumentError, 'Unexpected argument --n (did you mean -n?)')
82
+ end
83
+ it "fails with a double dash for short flags if argument does not exist" do
84
+ c = Razor::CLI::Command.new(nil, nil, {'schema' => {}},
85
+ ['--n', 'abc'], '/foobar')
86
+ expect{extract({'schema' => {}}, ['--n', 'abc'])}.
87
+ to raise_error(ArgumentError, 'Unexpected argument --n')
88
+ end
89
+ it "succeeds with a double dash for long flags" do
90
+ extract({'schema' => {'name' => {'type' => 'array'}}},
91
+ ['--name', 'abc'])['name'].should == ['abc']
92
+ end
93
+ it "succeeds with a single dash for short flags" do
94
+ c = Razor::CLI::Command.new(nil, nil, {'schema' => {'n' => {'type' => 'array'}}},
95
+ ['-n', 'abc'], nil)
96
+ extract({'schema' => {'n' => {'type' => 'array'}}}, ['-n', 'abc'])['n'].should == ['abc']
97
+ end
98
+ end
99
+
100
+ context "positional arguments" do
101
+ let(:schema) do
102
+ {'schema' => {'n' => {'position' => 1},
103
+ 'o' => {'position' => 0}}}
104
+ end
105
+ it "fails without a command schema" do
106
+ expect{extract(nil, ['123'])}.
107
+ to raise_error(ArgumentError, 'Unexpected argument 123')
108
+ end
109
+ it "fails if no positional arguments exist for a command" do
110
+ expect{extract({'schema' => {'n' => {}}}, ['abc'])}.
111
+ to raise_error(ArgumentError, 'Unexpected argument abc')
112
+ end
113
+ it "succeeds if no position is supplied" do
114
+ extract({'schema' => {'n' => {'position' => 0}}}, ['-n', '123'])['n'].
115
+ should == '123'
116
+ end
117
+ it "succeeds if position exists and is supplied" do
118
+ extract({'schema' => {'n' => {'position' => 0}}}, ['123'])['n'].
119
+ should == '123'
120
+ end
121
+ it "succeeds if multiple positions exist and are supplied" do
122
+ body = extract(schema, ['123', '456'])
123
+ body['o'].should == '123'
124
+ body['n'].should == '456'
125
+ end
126
+ it "fails if too many positions are supplied" do
127
+ expect{extract(schema, ['123', '456', '789'])}.
128
+ to raise_error(ArgumentError, 'Unexpected argument 789')
129
+ end
130
+ it "succeeds if multiple positions exist and one is supplied" do
131
+ body = extract(schema, ['123'])
132
+ body['o'].should == '123'
133
+ body['n'].should == nil
134
+ end
135
+ it "succeeds with a combination of positional and flags" do
136
+ body = extract(schema, ['123', '-n', '456'])
137
+ body['o'].should == '123'
138
+ body['n'].should == '456'
139
+ end
140
+ it "prefers the later between positional and flags" do
141
+ body = extract(schema, ['123', '-o', '456'])
142
+ body['o'].should == '456'
143
+ body = extract(schema, ['-o', '456', '123'])
144
+ body['o'].should == '123'
145
+ end
146
+ it "correctly sets datatypes" do
147
+ schema =
148
+ {'schema' => {'n' => {'type' => 'array', 'position' => 0},
149
+ 'o' => {'type' => 'number', 'position' => 1},
150
+ 'w' => {'type' => 'boolean', 'position' => 2},
151
+ 'a' => {'type' => 'object', 'position' => 3},
152
+ 'i' => {'type' => 'object', 'position' => 4}}}
153
+ body = extract(schema, ['arr', '123', 'true', '{}', 'abc=123'])
154
+ body['n'].should == ['arr']
155
+ body['o'].should == 123
156
+ body['w'].should == true
157
+ body['a'].should == {}
158
+ body['i'].should == {'abc' => '123'}
159
+ end
160
+ end
161
+ end
66
162
  end