razor-client 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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