sis_ruby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 584c4065727e96b0b4c0172fe31b6ab7d8a1d39a
4
+ data.tar.gz: 99dd517363e6e832f3e2c910f2b7a36ecb925a9e
5
+ SHA512:
6
+ metadata.gz: 001f9546fc5f481bb3b4677071f9d4dd924975810ab34ee25c83f3e749fb7ecfe367d5d74104bf04f76852fa05fb605f37c56c1e371ce9d3b7f063b623b75f91
7
+ data.tar.gz: 08a35bf1b678a39e0cb5b0826ee5c2c391c97fdec62df27e7c5833a61830ce3924fcb76f47228ff3757f9d0d7d1c71ab796104301e2885f6c041e4ff5d997e6f
data/.gitignore ADDED
@@ -0,0 +1,41 @@
1
+ ### Ruby template
2
+ *.gem
3
+ *.rbc
4
+ /.config
5
+ /coverage/
6
+ /InstalledFiles
7
+ /pkg/
8
+ /spec/reports/
9
+ /spec/examples.txt
10
+ /test/tmp/
11
+ /test/version_tmp/
12
+ /tmp/
13
+
14
+ ## Specific to RubyMotion:
15
+ .dat*
16
+ .repl_history
17
+ build/
18
+
19
+ ## Documentation cache and generated files:
20
+ /.yardoc/
21
+ /_yardoc/
22
+ /doc/
23
+ /rdoc/
24
+
25
+ ## Environment normalisation:
26
+ /.bundle/
27
+ /vendor/bundle
28
+ /lib/bundler/man/
29
+
30
+ # for a library or gem, you might want to ignore these files since the code is
31
+ # intended to run in multiple environments; otherwise, check them in:
32
+ # Gemfile.lock
33
+ # .ruby-version
34
+ # .ruby-gemset
35
+
36
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
37
+ .rvmrc
38
+
39
+ # Created by .ignore support plugin (hsz.mobi)
40
+
41
+ .idea/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ This software is licensed under the BSD 3-Clause license.
2
+
3
+ Copyright (c) 2016, Verisign, Inc. All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification,
6
+ are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation and/or
13
+ other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software without
17
+ specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
23
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # sis_ruby
2
+
3
+ A Ruby client to talk to a SIS server (https://github.com/sis-cmdb/sis-api).
4
+
5
+ Currently only the v1.1 API is supported.
6
+
7
+ Initial commit represents work by Neel Goyal, formerly at Verisign,
8
+ and Keith Bennett, currently at Verisign (http://www.verisign.com/).
9
+
10
+
11
+ ## Usage
12
+
13
+ Example code:
14
+
15
+ ```
16
+ require 'sis_ruby'
17
+
18
+ # Create a client
19
+ client = SisRuby::Client.new(sis_server_url)
20
+
21
+ # Get the host information endpoint
22
+ hosts = client.entities('host')
23
+
24
+ # Use all possible Params options to list records of a given entity:
25
+ params = SisRuby::Params.new.sort('hostname').offset(9000).limit(2).fields('hostname', 'environment')
26
+ records = hosts.list(params)
27
+
28
+ # Get record counts:
29
+ puts "Total host count is #{hosts.count}"
30
+ puts "Total qa host count is #{hosts.count('environment' => 'qa')}"
31
+
32
+ # Get up to 10 hooks
33
+ hook_list = client.hooks.list(SisRuby::Params.new.limit(10))
34
+
35
+ # Get 1 schema
36
+ schema_list = client.schemas.list(SisRuby::Params.new.limit(1)).first
37
+
38
+ # Hiera Entry Creation
39
+ hiera_entry = client.hiera.create({ 'name' => 'entry', 'hieradata' => { 'key1' => 'value1' }});
40
+
41
+ # Entity Deletion: Delete the entity of type 'entity_name' with id 'foo':
42
+ was_deleted = client.entities('entity_name').delete('foo');
43
+ ```
44
+
45
+ ## Client Initialization
46
+
47
+ The client constructor takes the following parameters:
48
+
49
+ * (required) a URL indicating the base URL of the SIS endpoints
50
+ * (optional) a hash containing neither, one, or both of:
51
+ * **:auth_token** - an authorization token field to be sent in the `x-auth-token` header
52
+ * **:api_version** - a version string to use in the request URL's instead of the default API version
53
+
54
+ ```
55
+ client = SisRuby::Client.new(sis_server_url)
56
+ client = SisRuby::Client.new(sis_server_url, auth_token: auth_token)
57
+ ```
58
+
59
+ ## Client Authentication
60
+
61
+ The client may also acquire and use a temporary token to use against the SIS endpoint
62
+ via the `authenticate` method. Below are examples:
63
+
64
+ ```
65
+ # This method will raise an SisRuby::Client::AuthenticationError if authentication fails.
66
+ token = client.authenticate(userid, password)
67
+
68
+ # You can also combine the client creation and authentication into a single expression
69
+ # since the authenticate method returns the client:
70
+ client = SisRuby::Client.new(sis_server_url).authenticate(userid, password)
71
+ ```
72
+
73
+ Although `authenticate` returns the token, there is no need to save it;
74
+ it is stored in the client instance for subsequent requests.
75
+
76
+ ## Entity, Hook, Schema, and Hiera Methods
77
+
78
+ The object returned by `client.hooks`, `client.schemas`, `client.hiera`, and
79
+ `client.entities(entity_name)` all interact with the appropriate endpoints
80
+ and expose the following interface:
81
+
82
+
83
+ ### list(query)
84
+
85
+ This maps to a GET `/` request against the appropriate endpoint for the default or specified API version.
86
+
87
+ List parameters can be specified in the form of any object responding to
88
+ `to_hash` and returning a hash (including, of course, a `Hash`).
89
+
90
+ Values in the hash can contain:
91
+
92
+ * **sort** - field name(s) on which to sort the records, precede fieldname with `-` for descending
93
+ * **limit** - limit the result set size to the specified number of records
94
+ * **fields** - only return the fields passed to this method; however:
95
+ * currently there is a bug that results in array type fields being returned even if they
96
+ are not included in the field list
97
+ * the _id field will always be returned even if it is not specified
98
+ * **offset** - the offset into the sorted results at which to start adding records to the result set
99
+ (Note: offset is not reliable unless a sort order is specified; there is no default sort order
100
+ and the random order may change from call to call.
101
+
102
+ For your convenience, a `Params` class has been provided with chainable methods (see code example).
103
+
104
+ Also, there is a `count` method that will return the total record count. If passed a filter, it will
105
+ return the count of records that meet the filter criteria.
106
+
107
+ An array of hashes is returned on success. For your convenience, there is also a
108
+ `list_as_openstructs` method that returns each record as an `OpenStruct` instance for easier access.
109
+ OpenStruct instances allow you to call methods whose names correspond to the original hash's keys,
110
+ e.g. `host.site` instead of `host['site']`.
111
+ This approach is not recommended for large numbers of records, as it requires more memory and processing.
112
+
113
+
114
+ ### get(id)
115
+
116
+ This maps to a GET `/id` request against the approprivate endpoint for the default or specified API version.
117
+
118
+ * id : a string representing the ID of the object to receive. For schemas, hooks, and hiera, this is the `name`.
119
+ For entities, it is the `_id`.
120
+
121
+ A single hash representing the object is returned on success.
122
+
123
+
124
+ ### create(obj)
125
+
126
+ This maps to a POST `/` request against the appropriate endpoint for the default or specified API version.
127
+
128
+ * obj : a valid Hash conforming to the endpoint specification
129
+
130
+ The created object as a hash is returned on success.
131
+
132
+
133
+ ### update(obj)
134
+
135
+ This maps to a PUT '/' request against the appropriate endpoint for the default or specified API version.
136
+
137
+ * obj : a valid hash conforming to the endpoint specification. Typically retrieved from `list` or `get`.
138
+
139
+ The updated hash representing the object is returned on success.
140
+
141
+
142
+ ### delete(obj)
143
+
144
+ This maps to a DELETE '/id' request against the appropriate endpoint for the default or specified API version.
145
+
146
+ * obj : either a string id or an object retrieved from `list` or `get`
147
+
148
+ The boolean `true` is returned on success.
149
+
150
+
151
+ ## Error handling
152
+
153
+ An instance of `SisRuby::Client` will raise an exception if an HTTP request returns a status code above and including 400.
154
+
155
+
156
+ ## Running the Tests
157
+
158
+ Test code is divided into different top-level directories:
159
+
160
+ ### `spec`
161
+
162
+ These are tests not requiring a SIS server.
163
+
164
+ ### `spec_reading`
165
+
166
+ These are tests requiring a SIS server containing a 'host' entity populated with records
167
+
168
+ ### `spec_writing`
169
+
170
+ These are tests requiring a SIS server that can be used for writing; for these
171
+ you'll probably want to set up a local server. Instructions for this are at
172
+ https://github.com/sis-cmdb/sis-api#building-and-testing.
173
+
174
+ If the tests should ever fail and the data base is not correctly restored to
175
+ its original empty state, you can delete the Mongo data
176
+ by running the Mongo shell ('mongo') and issuing the following commands:
177
+
178
+ ```
179
+ use test
180
+ db.dropDatabase();
181
+ ```
182
+
183
+ To verify the user id and password, you can issue the following command, replacing 'test' and
184
+ 'abc123' with the real user id and password:
185
+
186
+ ```
187
+ curl -D- -u test:abc123 -X POST -H "Content-Type: application/json" http://localhost:3000/api/v1.1/users/auth_token
188
+ ```
data/RELEASE_NOTES.md ADDED
@@ -0,0 +1,3 @@
1
+ ## v1.0.0
2
+
3
+ Code base copied from @ngoyal's original version internal to Verisign, with extensive modifications.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ # From https://github.com/goncalossilva/gem_template
2
+ begin
3
+ require "bundler"
4
+ Bundler.setup
5
+ rescue LoadError
6
+ $stderr.puts "You need to have Bundler installed to be able build this gem."
7
+ end
8
+
9
+ gemspec = eval(File.read('sis_ruby.gemspec'))
10
+
11
+ desc "Validate the gemspec"
12
+ task :gemspec do
13
+ gemspec.validate
14
+ end
15
+
16
+ desc "Build gem locally"
17
+ task :build => :gemspec do
18
+ system "gem build #{gemspec.name}.gemspec"
19
+ FileUtils.mkdir_p "pkg"
20
+ FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "pkg"
21
+ end
22
+
23
+ desc "Install gem locally"
24
+ task :install => :build do
25
+ system "gem install pkg/#{gemspec.name}-#{gemspec.version}.gem"
26
+ end
27
+
28
+ desc "Clean automatically generated files"
29
+ task :clean do
30
+ FileUtils.rm_rf "pkg"
31
+ end
32
+
33
+ require 'rspec/core/rake_task'
34
+
35
+ RSpec::Core::RakeTask.new('spec')
@@ -0,0 +1,85 @@
1
+ require_relative 'common'
2
+ require 'sis_ruby/endpoint'
3
+ require 'typhoeus'
4
+
5
+ module SisRuby
6
+
7
+ class Client
8
+
9
+ attr_reader :api_version, :auth_token, :base_url, :entity_endpoints, :hiera, :hooks, :hosts, :schemas
10
+
11
+ DEFAULT_API_VERSION = '1.1'
12
+
13
+ # @param url the base URL of the service (excluding ''/api/v...'')
14
+ # @param options (can include :version, :auth_token)
15
+ def initialize(url, options = {})
16
+ @base_url = url
17
+ @api_version = options[:api_version] || DEFAULT_API_VERSION
18
+ @auth_token = options[:auth_token]
19
+
20
+ @entity_endpoints = {}
21
+ @hooks = create_endpoint('hooks', 'name')
22
+ @schemas = create_endpoint('schemas', 'name')
23
+ @hiera = create_endpoint('hiera', 'name')
24
+ end
25
+
26
+
27
+ def create_endpoint(endpoint_suffix, id_fieldname = :default)
28
+ Endpoint.new(self, endpoint_suffix, id_fieldname)
29
+ end
30
+
31
+
32
+ def entities(name, id_fieldname = DEFAULT_ID_FIELDNAME)
33
+ @entity_endpoints[name] ||= create_endpoint("entities/#{name}", id_fieldname)
34
+ end
35
+
36
+
37
+ def tokens(username)
38
+ create_endpoint("users/#{username}/tokens", 'name')
39
+ end
40
+
41
+
42
+ # Returns the schema for the specified collection_name, or nil if it's not found.
43
+ def schema_for(collection_name)
44
+ params = Params.new.limit(1).filter('name' => collection_name)
45
+ schemas.list(params).first
46
+ end
47
+
48
+
49
+ # Authenticates the username and password. Get the token by calling client.auth_token.
50
+ # @return self for chaining this method after the constructor
51
+ def authenticate(username, password)
52
+ dest = "#{base_url}/api/v#{api_version}/users/auth_token"
53
+ options = {
54
+ userpwd: username + ':' + password,
55
+ headers: { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
56
+ }
57
+ response = Typhoeus.post(dest, options)
58
+ unless response.options[:response_code] == 201
59
+ raise AuthenticationError.new(response)
60
+ end
61
+
62
+ @auth_token = JSON.parse(response.response_body)['name']
63
+ self
64
+ end
65
+
66
+
67
+ def to_s
68
+ self.class.name + ": base_url = #{@base_url}"
69
+ end
70
+
71
+
72
+
73
+ class AuthenticationError < Exception
74
+
75
+ def initialize(response)
76
+ @response = response
77
+ end
78
+
79
+ def to_s
80
+ @response.options[:response_headers].split("\r\n").first
81
+ end
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,5 @@
1
+ module SisRuby
2
+
3
+ DEFAULT_ID_FIELDNAME = '_id'
4
+
5
+ end
@@ -0,0 +1,108 @@
1
+ require 'typhoeus'
2
+ require 'json'
3
+
4
+ require_relative 'client'
5
+ require_relative 'common'
6
+ require_relative 'get_helper'
7
+ require_relative 'result_enumerable'
8
+ require_relative 'exceptions/missing_id_error'
9
+
10
+ module SisRuby
11
+
12
+ class Endpoint
13
+
14
+ include GetHelper
15
+
16
+ attr_reader :client, :id_field, :url
17
+
18
+
19
+ def initialize(client, endpoint_name, id_field = DEFAULT_ID_FIELDNAME)
20
+ @client = client
21
+ @url = "#{client.base_url}/api/v#{client.api_version}/#{endpoint_name}"
22
+ @id_field = id_field
23
+ end
24
+
25
+
26
+ # This method is used to allow callers to pass either the id itself,
27
+ # or the record including an id key/value pair.
28
+ def id_from_param(object)
29
+ id = object.is_a?(Hash) ? object[@id_field] : object
30
+ id ? id : raise(MissingIdError.new("Missing required id field #{@id_field}}"))
31
+ end
32
+
33
+
34
+ def create_enumerable(params = {}, chunk_size = ResultEnumerable::DEFAULT_CHUNK_RECORD_COUNT)
35
+ ResultEnumerable.new(self, params, chunk_size)
36
+ end
37
+
38
+
39
+ # Gets the total count of records, with optional filter
40
+ def count(filter = {})
41
+ params = Params.new.filter(filter).limit(1).to_hash
42
+ response = Typhoeus::Request.new(@url, params: params, headers: create_headers(true)).run
43
+ response.headers['x-total-count'].to_i
44
+ end
45
+
46
+
47
+ # Anything implementing a to_hash method can be passed as the query.
48
+ # This enables the passing in of SisParams objects.
49
+ def list(params = {})
50
+ create_enumerable(params).each.to_a
51
+ end
52
+
53
+
54
+ # Anything implementing a to_hash method can be passed as the query.
55
+ # This enables the passing in of SisParams objects.
56
+ def list_as_openstructs(query = {})
57
+ list(query).map { |h| OpenStruct.new(h) }
58
+ end
59
+
60
+
61
+ def get(id)
62
+ request = Typhoeus::Request.new("#{url}/#{id}", headers: create_headers(true))
63
+ response = request.run
64
+ validate_response_success(response)
65
+ JSON.parse(response.body)
66
+ end
67
+
68
+
69
+ def create(obj)
70
+ http_response = Typhoeus.post(@url, { body: obj.to_json, headers: get_headers(true) } )
71
+ validate_response_success(http_response)
72
+ JSON.parse(http_response.body)
73
+ end
74
+
75
+
76
+ def delete(id)
77
+ id = id_from_param(id)
78
+ http_response = Typhoeus.delete("#{@url}/#{id}", headers: get_headers(false))
79
+ validate_response_success(http_response)
80
+ JSON.parse(http_response.body)
81
+ end
82
+
83
+
84
+ def update(obj)
85
+ id = id_from_param(obj)
86
+ http_response = self.class.put("#{@url}/#{id}", {body: obj.to_json, headers: get_headers(true) })
87
+ validate_response_success(http_response)
88
+ JSON.parse(http_response.body)
89
+ end
90
+
91
+
92
+ def to_s
93
+ self.class.name + ": endpoint = #{@url}, client = #{@client}"
94
+ end
95
+
96
+
97
+ def ==(other)
98
+ client.equal?(other.client) && url == other.url && id_field == other.id_field
99
+ end
100
+
101
+ private
102
+
103
+ def get_headers(specify_content_type)
104
+ create_headers(specify_content_type, @client.auth_token)
105
+ end
106
+ end
107
+
108
+ end
@@ -0,0 +1,35 @@
1
+ module SisRuby
2
+
3
+ class BadResponseError < RuntimeError
4
+
5
+ attr_reader :response
6
+
7
+ def initialize(response)
8
+ @response = response
9
+ end
10
+
11
+
12
+ def to_s
13
+
14
+ first_header_line = if response && response.options[:response_headers]
15
+ header_lines = response.options[:response_headers].split("\r\n")
16
+ header_lines.any? ? header_lines.first : nil
17
+ else
18
+ nil
19
+ end
20
+
21
+ body = if response && response.options[:response_body]
22
+ response.options[:response_body].rstrip
23
+ else
24
+ nil
25
+ end
26
+
27
+ code = response.code
28
+
29
+ string = "#{self.class.name}: #{code}"
30
+ string << ": #{body}" if body
31
+ string << (first_header_line ? " (#{first_header_line})" : '')
32
+ string
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ module SisRuby
2
+ class MissingIdError < RuntimeError
3
+
4
+ def initialize(id_field_name)
5
+ @id_field_name = id_field_name
6
+ end
7
+
8
+ def to_s
9
+ "Missing required id field #{@id_field_name}}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'exceptions/bad_response_error'
2
+
3
+
4
+ # Methods for helping get data from SIS.
5
+ module SisRuby
6
+ module GetHelper
7
+
8
+ # Creates the header for a request.
9
+ def create_headers(specify_content_type, auth_token = nil)
10
+ headers = {
11
+ 'Accept' => 'application/json'
12
+ }
13
+
14
+ if auth_token
15
+ headers['x-auth-token'] = auth_token
16
+ end
17
+
18
+ if specify_content_type
19
+ headers['Content-Type'] = 'application/json'
20
+ end
21
+
22
+ headers
23
+ end
24
+
25
+
26
+ # Raises an error on response failure.
27
+ def validate_response_success(response)
28
+ unless response.code.between?(200, 299)
29
+ raise BadResponseError.new(response)
30
+ end
31
+ end
32
+
33
+
34
+ # Returns a Typhoeus response.
35
+ def typhoeus_get(query)
36
+ # TODO: Simplify w/Typhoeus.get ?
37
+ Typhoeus::Request.new(url, params: query, headers: get_headers(true) ).run
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+
2
+ # Takes methods and builds an internal hash where the method calls
3
+ # are keys, and their parameter is the value.
4
+ class HashBuilder
5
+
6
+ attr_reader :key_type
7
+
8
+ def initialize(key_type = String)
9
+ raise ArgumentError.new("Invalid key type '#{key_type}'") unless [String, Symbol].include?(key_type)
10
+ @key_type = key_type
11
+ @data = {}
12
+ end
13
+
14
+
15
+ def method_missing(method_name, *args)
16
+ value = args.first
17
+ key = (@key_type == String) ? method_name.to_s : method_name.to_sym
18
+ @data[key] = value
19
+ self
20
+ end
21
+
22
+
23
+ def respond_to_missing?(method_name, include_private)
24
+ true # TODO: exclude ancestor methods?
25
+ end
26
+
27
+
28
+ def to_h
29
+ @data
30
+ end
31
+ end