Skip to content

Commit

Permalink
Merge pull request #87 from HewlettPackard/cli_rest
Browse files Browse the repository at this point in the history
Added rest and update CLI commands
  • Loading branch information
jsmartt authored Sep 15, 2016
2 parents a5cfd68 + 98f2a79 commit ecd5bbc
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 36 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### v2.2.0
- Added the 'rest' and 'update' commands to the CLI
- Removed the --force option from the create_from_file CLI command

### v2.1.0
- Fixed issue with the :resource_named method for OneViewSDK::Resource in Ruby 2.3

Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Each OneView resource is exposed for usage with CRUD-like functionality.

For example, once you instantiate a resource object, you can call intuitive methods such as `resource.create`, `resource.udpate` and `resource.delete`. In addition, resources respond to helpful methods such as `.each`, `.eql?(other_resource)`, `.like(other_resource)`, `.retrieve!`, and many others.

Please see the [rubydoc.info](http://www.rubydoc.info/gems/oneview-sdk) documentation for the complete list and usage details, but here are a few examples to get you started:
Please see the [rubydoc.info](http://www.rubydoc.info/gems/oneview-sdk) documentation for complete usage details and the [examples](examples/) directory for more examples and test-scripts, but here are a few examples to get you started:

##### Create a resource

Expand Down Expand Up @@ -264,6 +264,20 @@ $ oneview-sdk-ruby create_from_file /my-server-profile.json
$ oneview-sdk-ruby delete_from_file /my-server-profile.json
```

##### Update resources by name:

```bash
$ oneview-sdk-ruby update FCNetwork FC1 -h linkStabilityTime:20 # Using hash format
$ oneview-sdk-ruby update Volume VOL_01 -j '{"shareable": true}' # Using json format
```

##### Make REST calls:

```bash
$ oneview-sdk-ruby rest get rest/fc-networks
$ oneview-sdk-ruby rest PUT rest/enclosures/<id>/configuration
```

##### Start an interactive console session with a OneView connection:

```bash
Expand Down
89 changes: 79 additions & 10 deletions lib/oneview-sdk/cli.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# (C) Copyright 2016 Hewlett Packard Enterprise Development LP
# (c) Copyright 2016 Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# You may not use this file except in compliance with the License.
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed
Expand Down Expand Up @@ -201,6 +201,80 @@ def search(type)
end
end

method_option :format,
desc: 'Output format',
aliases: '-f',
enum: %w(json yaml raw),
default: 'json'
method_option :data,
desc: 'Data to pass in the request body (in JSON format)',
aliases: '-d'
rest_examples = "\n oneview-sdk-ruby rest GET rest/fc-networks"
rest_examples << "\n oneview-sdk-ruby rest PUT rest/fc-networks/<id> -d '{\"linkStabilityTime\": 20, ...}'"
rest_examples << "\n oneview-sdk-ruby rest PUT rest/enclosures/<id>/configuration"
desc 'rest METHOD URI', "Make REST call to the OneView API. Examples:#{rest_examples}"
def rest(method, uri)
client_setup('log_level' => :error)
uri_copy = uri.dup
uri_copy.prepend('/') unless uri_copy.start_with?('/')
if @options['data']
begin
data = { body: JSON.parse(@options['data']) }
rescue JSON::ParserError => e
fail_nice("Failed to parse data as JSON\n#{e.message}")
end
end
data ||= {}
response = @client.rest_api(method, uri_copy, data)
if response.code.to_i.between?(200, 299)
case @options['format']
when 'yaml'
puts JSON.parse(response.body).to_yaml
when 'json'
puts JSON.pretty_generate(JSON.parse(response.body))
else # raw
puts response.body
end
else
body = JSON.pretty_generate(JSON.parse(response.body)) rescue response.body
fail_nice("Request failed: #{response.inspect}\nHeaders: #{response.to_hash}\nBody: #{body}")
end
rescue OneviewSDK::InvalidRequest => e
fail_nice(e.message)
end

method_option :hash,
type: :hash,
desc: 'Hash of key/value pairs to update',
aliases: '-h'
method_option :json,
desc: 'JSON data to pass in the request body',
aliases: '-j'
update_examples = "\n oneview-sdk-ruby update FCNetwork FC1 -h linkStabilityTime:20"
update_examples << "\n oneview-sdk-ruby update Volume VOL1 -j '{\"shareable\": true}'"
desc 'update TYPE NAME --[hash|json] <data>', "Update resource by name. Examples:#{update_examples}"
def update(type, name)
resource_class = parse_type(type)
client_setup
fail_nice 'Must set the hash or json option' unless @options['hash'] || @options['json']
fail_nice 'Must set the hash OR json option. Not both' if @options['hash'] && @options['json']
begin
data = @options['hash'] || JSON.parse(@options['json'])
rescue JSON::ParserError => e
fail_nice("Failed to parse json\n#{e.message}")
end
matches = resource_class.find_by(@client, name: name)
fail_nice 'Not Found' if matches.empty?
resource = matches.first
begin
resource[:uri] = '/rest/storage-volumes/57A22A70-73EC-43C1-91B9-9FABD1E'
resource.update(data)
output 'Updated Successfully!'
rescue StandardError => e
fail_nice "Failed to update #{resource.class.name.split('::').last} '#{name}': #{e}"
end
end

method_option :force,
desc: 'Delete without confirmation',
type: :boolean,
Expand Down Expand Up @@ -245,17 +319,12 @@ def delete_from_file(file_path)
end
end

method_option :force,
desc: 'Overwrite without confirmation',
type: :boolean,
aliases: '-f'
method_option :if_missing,
desc: 'Only create if missing (Don\'t update)',
type: :boolean,
aliases: '-i'
desc 'create_from_file FILE_PATH', 'Create/Overwrite resource defined in file'
desc 'create_from_file FILE_PATH', 'Create/Update resource defined in file'
def create_from_file(file_path)
fail_nice "Can't use the 'force' and 'if_missing' flags at the same time." if options['force'] && options['if_missing']
client_setup
resource = OneviewSDK::Resource.from_file(@client, file_path)
resource[:uri] = nil
Expand All @@ -266,7 +335,6 @@ def create_from_file(file_path)
puts "Skipped: '#{resource[:name]}': #{resource.class.name.split('::').last} already exists."
return
end
fail_nice "#{resource.class.name.split('::').last} '#{resource[:name]}' already exists." unless options['force']
begin
resource.data.delete('uri')
existing_resource.update(resource.data)
Expand Down Expand Up @@ -338,7 +406,8 @@ def parse_type(type)
next unless klass.is_a?(Class) && klass < OneviewSDK::Resource
valid_classes.push(klass.name.split('::').last)
end
OneviewSDK.resource_named(type) || fail_nice("Invalid resource type: '#{type}'.\n Valid options are #{valid_classes}")
vc = valid_classes.sort_by!(&:downcase).join("\n ")
OneviewSDK.resource_named(type) || fail_nice("Invalid resource type: '#{type}'. Valid options are:\n #{vc}")
end

# Parse options hash from input. Handles chaining and keywords such as true/false & nil
Expand Down
2 changes: 1 addition & 1 deletion lib/oneview-sdk/rest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def build_request(type, uri, options, api_ver)
when :delete
request = Net::HTTP::Delete.new(uri.request_uri)
else
fail InvalidRequest, "Invalid rest call: #{type}"
fail InvalidRequest, "Invalid rest method: #{type}. Valid methods are: get, post, put, patch, delete"
end

options['X-API-Version'] ||= api_ver
Expand Down
2 changes: 1 addition & 1 deletion lib/oneview-sdk/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@

# Gem version defined here
module OneviewSDK
VERSION = '2.1.0'.freeze
VERSION = '2.2.0'.freeze
end
4 changes: 2 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
add_group 'Client', client_files
add_group 'Resources', resource_path
add_group 'CLI', 'cli.rb'
minimum_coverage 90 # TODO: bump up as we increase coverage. Goal: 95%
minimum_coverage_by_file 50 # TODO: bump up as we increase coverage. Goal: 70%
minimum_coverage 92 # TODO: bump up as we increase coverage. Goal: 95%
minimum_coverage_by_file 60 # TODO: bump up as we increase coverage. Goal: 70%
end

SimpleCov.profiles.define 'integration' do
Expand Down
23 changes: 3 additions & 20 deletions spec/unit/cli/create_from_file_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@
expect { OneviewSDK::Cli.start(['create_from_file']) }
.to output(/was called with no arguments*\sUsage:/).to_stderr_from_any_process
end

it 'does not allow both the if_missing and force options' do
expect(STDOUT).to receive(:puts).with(/flags at the same time/)
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file, '-f', '-i']) }
.to raise_error SystemExit
end
end

context 'with valid options' do
Expand All @@ -36,32 +30,21 @@
.to output(/Created Successfully!/).to_stdout_from_any_process
end

it 'respects the force option for overrides' do
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file, '-f']) }
.to output(/Updated Successfully!/).to_stdout_from_any_process
end

it 'respects the if_missing option' do
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file, '-i']) }
.to output(/Skipped/).to_stdout_from_any_process
end

it 'fails if the resource already exists' do
expect(STDOUT).to receive(:puts).with(/already exists/)
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file]) }
.to raise_error SystemExit
end

it 'fails if the file does not exist' do
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file + '.yml', '-f']) }
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file + '.yml']) }
.to raise_error(/No such file or directory/)
end

it 'fails if the file does not specify a name' do
resource = OneviewSDK::Resource.new(@client)
allow(OneviewSDK::Resource).to receive(:from_file).and_return(resource)
expect(STDOUT).to receive(:puts).with(/must specify a resource name/)
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file, '-f']) }
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file]) }
.to raise_error SystemExit
end

Expand All @@ -76,7 +59,7 @@
it 'shows the resource update error message on failure' do
allow_any_instance_of(OneviewSDK::Resource).to receive(:update).and_raise('Explanation')
expect(STDOUT).to receive(:puts).with(/Failed to update.*Explanation/)
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file, '-f']) }
expect { OneviewSDK::Cli.start(['create_from_file', yaml_file]) }
.to raise_error SystemExit
end
end
Expand Down
124 changes: 124 additions & 0 deletions spec/unit/cli/rest_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
require 'spec_helper'

RSpec.describe OneviewSDK::Cli do
include_context 'cli context'

let(:data) { { 'key1' => 'val1', 'key2' => 'val2' } }
let(:response) { { 'key1' => 'val1', 'key2' => 'val2', 'key3' => { 'key4' => 'val4' } } }

describe '#rest get' do
it 'requires a URI' do
expect($stderr).to receive(:puts).with(/ERROR.*arguments/)
described_class.start(%w(rest get))
end

it 'sends any data that is passed in' do
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('get', '/rest/fake', body: data).and_return FakeResponse.new(response)
expect { described_class.start(['rest', 'get', 'rest/fake', '-d', data.to_json]) }
.to output(JSON.pretty_generate(response) + "\n").to_stdout_from_any_process
end

context 'output formats' do
before :each do
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('get', '/rest/fake', {}).and_return FakeResponse.new(response)
end

it 'makes a GET call to the URI and outputs the response in json format' do
expect { described_class.start(%w(rest get rest/fake)) }
.to output(JSON.pretty_generate(response) + "\n").to_stdout_from_any_process
end

it 'can output the response in raw format' do
expect { described_class.start(%w(rest get rest/fake -f raw)) }
.to output(response.to_json + "\n").to_stdout_from_any_process
end

it 'can output the response in yaml format' do
expect { described_class.start(%w(rest get rest/fake -f yaml)) }
.to output(response.to_yaml).to_stdout_from_any_process
end
end

context 'bad requests' do
it 'fails if the data cannot be parsed as json' do
expect($stdout).to receive(:puts).with(/Failed to parse data as JSON/)
expect { described_class.start(%w(rest get rest/ -d fake_json)) }
.to raise_error SystemExit
end

it 'fails if the request method is invalid' do
expect($stdout).to receive(:puts).with(/Invalid rest method/)
expect { described_class.start(%w(rest blah rest/)) }
.to raise_error SystemExit
end

it 'fails if the response code is 3XX' do
headers = { 'location' => ['rest/Systems/1/'] }
body = {}
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('get', '/rest', {}).and_return FakeResponse.new(body, 308, headers)
expect($stdout).to receive(:puts).with(/308.*location/m)
expect { described_class.start(%w(rest get rest)) }
.to raise_error SystemExit
end

it 'fails if the response code is 4XX' do
headers = { 'content-type' => ['text/plain'] }
body = { 'Message' => 'Not found!' }
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('get', '/rest', {}).and_return FakeResponse.new(body, 404, headers)
expect($stdout).to receive(:puts).with(/404.*content-type.*Not found/m)
expect { described_class.start(%w(rest get rest)) }
.to raise_error SystemExit
end

it 'fails if the response code is 4XX' do
headers = { 'content-type' => ['text/plain'] }
body = { 'Message' => 'Server error!' }
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('get', '/rest', {}).and_return FakeResponse.new(body, 500, headers)
expect($stdout).to receive(:puts).with(/500.*content-type.*Server error/m)
expect { described_class.start(%w(rest get rest)) }
.to raise_error SystemExit
end
end
end

describe '#rest post' do
it 'makes a rest call with any data that is passed in' do
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('post', '/rest/fake', body: data).and_return FakeResponse.new(response)
expect { described_class.start(['rest', 'post', 'rest/fake', '-d', data.to_json]) }
.to output(JSON.pretty_generate(response) + "\n").to_stdout_from_any_process
end
end

describe '#rest put' do
it 'makes a rest call with any data that is passed in' do
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('put', '/rest/fake', body: data).and_return FakeResponse.new(response)
expect { described_class.start(['rest', 'put', 'rest/fake', '-d', data.to_json]) }
.to output(JSON.pretty_generate(response) + "\n").to_stdout_from_any_process
end
end

describe '#rest patch' do
it 'makes a rest call with any data that is passed in' do
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('patch', '/rest/fake', body: data).and_return FakeResponse.new(response)
expect { described_class.start(['rest', 'patch', 'rest/fake', '-d', data.to_json]) }
.to output(JSON.pretty_generate(response) + "\n").to_stdout_from_any_process
end
end

describe '#rest delete' do
it 'makes a rest call with any data that is passed in' do
expect_any_instance_of(OneviewSDK::Client).to receive(:rest_api)
.with('delete', '/rest/fake', body: data).and_return FakeResponse.new(response)
expect { described_class.start(['rest', 'delete', 'rest/fake', '-d', data.to_json]) }
.to output(JSON.pretty_generate(response) + "\n").to_stdout_from_any_process
end
end
end
Loading

0 comments on commit ecd5bbc

Please sign in to comment.