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