pe-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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 699354279b91bfa3600d9b7c0cda040067a5b9f8
4
- data.tar.gz: 929d073ad3f2272b1c5175bf5e664d09d273aba6
3
+ metadata.gz: fe070f4bc93ec0248752bc35b80c29e24df6461a
4
+ data.tar.gz: 8f0a33e1f9b6e5a29421c6e1fd52055affd1c1f0
5
5
  SHA512:
6
- metadata.gz: 1c82a4c3b8c71c37a7f660fa638cb753801d41700f25f7d136c3a8a1229d74e94b068929da845cc5bbae97718897ccd881102251ea55459b7acd8d61fac6747d
7
- data.tar.gz: a6f7638bfd1af09182b9f25273e90d3e5a80cd21c711d7f4df4b9797c6faf6f9bf2bdbcaf74b41fd67425d93943dfa9d2891271e12bd3e7f7d8872d163097c81
6
+ metadata.gz: 80e55e7decd173351ffbf41a45bad53f2a2c60e3b7d75a03a974dabc100bddf6117dbaf92d7e844022027ec98f0f1e2bc9f248142254bae716b40c071b214b5c
7
+ data.tar.gz: 53be116318939b11783b775ddbb1849ea291f06d55aada79d6a7cbea64c587f240eddf09ae9a6e2af968a0bd6dfa6075d1bdfb622f8e1ac5280f931acb5afec9
data/NEWS.md CHANGED
@@ -1,8 +1,22 @@
1
1
  # Razor Client Release Notes
2
2
 
3
- ## Next - Next
3
+ ## 1.2.0 - 2016-03-08
4
4
 
5
- * IMPROVEMENT: By default, `razor` will point to port 8151.
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
+
16
+ ## 1.1.0 - 2015-11-12
17
+
18
+ * IMPROVEMENT: By default, `razor` will point to port 8150.
19
+ * IMPROVEMENT: Better display of several views/collections.
6
20
 
7
21
  ## 1.0.0 - 2015-06-08
8
22
 
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(/^/, ' ')}
@@ -3,6 +3,42 @@ require 'multi_json'
3
3
  require 'yaml'
4
4
  require 'forwardable'
5
5
 
6
+ # Needed to make the client work on Ruby 1.8.7
7
+ unless URI.respond_to?(:encode_www_form)
8
+ module URI
9
+ def self.encode_www_form_component(str)
10
+ str = str.to_s
11
+ if HTML5ASCIIINCOMPAT.include?(str.encoding)
12
+ str = str.encode(Encoding::UTF_8)
13
+ else
14
+ str = str.dup
15
+ end
16
+ str.force_encoding(Encoding::ASCII_8BIT)
17
+ str.gsub!(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_)
18
+ str.force_encoding(Encoding::US_ASCII)
19
+ end
20
+ def self.encode_www_form(enum)
21
+ enum.map do |k,v|
22
+ if v.nil?
23
+ encode_www_form_component(k)
24
+ elsif v.respond_to?(:to_ary)
25
+ v.to_ary.map do |w|
26
+ str = encode_www_form_component(k)
27
+ unless w.nil?
28
+ str << '='
29
+ str << encode_www_form_component(w)
30
+ end
31
+ end.join('&')
32
+ else
33
+ str = encode_www_form_component(k)
34
+ str << '='
35
+ str << encode_www_form_component(v)
36
+ end
37
+ end.join('&')
38
+ end
39
+ end
40
+ end
41
+
6
42
  module Razor::CLI
7
43
  class Navigate
8
44
  extend Forwardable
@@ -57,7 +93,15 @@ module Razor::CLI
57
93
  elsif query?
58
94
  Razor::CLI::Query.new(@parse, self, collections, @segments).run
59
95
  elsif command?
60
- 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
61
105
  else
62
106
  raise NavigationError.new(@doc_resource, @segments, @doc)
63
107
  end
@@ -2,6 +2,16 @@ require 'uri'
2
2
  require 'optparse'
3
3
  require 'forwardable'
4
4
 
5
+ # Needed to make the client work on Ruby 1.8.7
6
+ unless URI::Generic.method_defined?(:hostname)
7
+ module URI
8
+ def hostname
9
+ v = self.host
10
+ /\A\[(.*)\]\z/ =~ v ? $1 : v
11
+ end
12
+ end
13
+ end
14
+
5
15
  module Razor::CLI
6
16
 
7
17
  class Parse
@@ -53,22 +63,19 @@ module Razor::CLI
53
63
  end
54
64
 
55
65
  def version
66
+ server_version = '(unknown)'
67
+ error = ''
56
68
  begin
57
- <<-VERSION
58
- Razor Server version: #{navigate.server_version}
59
- Razor Client version: #{Razor::CLI::VERSION}
60
- VERSION
69
+ server_version = navigate.server_version
61
70
  rescue RestClient::Unauthorized
62
- puts <<-UNAUTH
63
- Error: Credentials are required to connect to the server at #{@api_url}"
64
- UNAUTH
65
- exit 1
71
+ error = "Error: Credentials are required to connect to the server at #{@api_url}."
66
72
  rescue
67
- puts <<-ERR
68
- Error: Could not connect to the server at #{@api_url}. More help is available after pointing
69
- the client to a Razor server
70
- ERR
71
- 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
72
79
  end
73
80
  end
74
81
 
@@ -95,10 +102,23 @@ HELP
95
102
  Error: Credentials are required to connect to the server at #{@api_url}"
96
103
  UNAUTH
97
104
  exit = 1
98
- 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
99
119
  output << <<-ERR
100
- Error: Could not connect to the server at #{@api_url}. More help is available after pointing
101
- the client to a Razor server
120
+ Error: Unknown error occurred while connecting to server at #{@api_url}:
121
+ #{e}
102
122
  ERR
103
123
  exit = 1
104
124
  end
@@ -133,23 +153,34 @@ ERR
133
153
  # The format can be determined from later segments.
134
154
  attr_accessor :format, :stripped_args, :ssl_ca_file
135
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'
136
158
  def initialize(args)
137
159
  parse_and_set_api_url(ENV["RAZOR_API"] || DEFAULT_RAZOR_API, :env)
138
160
  @args = args.dup
139
161
  # To be populated externally.
140
162
  @stripped_args = []
141
163
  @format = 'short'
164
+ @verify_ssl = true
165
+ env_pem_file = ENV['RAZOR_CA_FILE']
142
166
  # If this is set, it should actually exist.
143
- if ENV['RAZOR_CA_FILE'] && !File.exists?(ENV['RAZOR_CA_FILE'])
144
- 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
145
176
  end
146
- ca_file = ENV["RAZOR_CA_FILE"] || '/etc/puppetlabs/puppet/ssl/certs/ca.pem'
147
- @ssl_ca_file = ca_file if ca_file && File.exists?(ca_file)
148
177
  @args = get_optparse.order(@args)
149
178
 
150
179
  # Localhost won't match the server's certificate; no verification required.
151
180
  # This needs to happen after get_optparse so `-k` and `-u` can take effect.
152
- @verify_ssl ||= (@api_url.hostname != 'localhost')
181
+ if @api_url.hostname == 'localhost'
182
+ @verify_ssl = false
183
+ end
153
184
 
154
185
  @args = set_help_vars(@args)
155
186
  if @args == ['version'] or @show_version
@@ -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