sis_ruby 1.0.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.
@@ -0,0 +1,117 @@
1
+ module SisRuby
2
+ class Params
3
+
4
+ # These methods behave as simple accessors when called with no arguments.
5
+ # When called with an argument, that argument is used to set the value corresponding to the method name.
6
+
7
+ # The conventional 'attribute=' method forms for this class are supported
8
+ # in addition to the methods below, as an alternate way to set values.
9
+
10
+ attr_writer :fields, :filter, :limit, :offset, :sort
11
+
12
+ def fields(*args)
13
+ if args.none?
14
+ return @fields
15
+ elsif args.one?
16
+ @fields = Array(args.first)
17
+ else
18
+ @fields = args
19
+ end
20
+ @fields = @fields.to_set # order shouldn't matter, and no dups
21
+ self
22
+ end
23
+
24
+
25
+ def filter(*args)
26
+ if args.any?
27
+ @filter = args.first
28
+ self
29
+ else
30
+ @filter
31
+ end
32
+ end
33
+
34
+
35
+ def limit(*args)
36
+ if args.any?
37
+ @limit = args.first
38
+ self
39
+ else
40
+ @limit
41
+ end
42
+ end
43
+
44
+
45
+ def offset(*args)
46
+ if args.any?
47
+ @offset = args.first
48
+ self
49
+ else
50
+ @offset
51
+ end
52
+ end
53
+
54
+
55
+ def sort(*args)
56
+ if args.any?
57
+ @sort = args.first
58
+ self
59
+ else
60
+ @sort
61
+ end
62
+ end
63
+
64
+
65
+ def to_hash
66
+ h = {}
67
+ h['limit'] = limit if limit
68
+ h['offset'] = offset if offset
69
+ h['fields'] = fields.to_a.join(',') if fields
70
+ h['q'] = filter if filter
71
+ h['sort'] = sort if sort
72
+ h
73
+ end
74
+
75
+
76
+ def self.from_hash(other)
77
+ instance = self.new
78
+ instance.limit(other['limit']) if other['limit']
79
+ instance.fields(other['fields'].split(',').to_set) if other['fields']
80
+ instance.offset(other['offset']) if other['offset']
81
+ instance.filter(other['q']) if other['q']
82
+ instance.sort(other['sort']) if other['sort']
83
+ instance
84
+ end
85
+
86
+
87
+ def clone
88
+ other = Params.new
89
+ other.limit(self.limit) if self.limit
90
+ other.fields(self.fields.clone) if self.fields
91
+ other.offset(self.offset) if self.offset
92
+ other.filter(self.filter.clone) if self.filter
93
+ other.sort(self.sort) if self.sort
94
+ other
95
+ end
96
+
97
+
98
+ def to_h
99
+ to_hash
100
+ end
101
+
102
+
103
+ def hash
104
+ to_h.hash
105
+ end
106
+
107
+
108
+ def ==(other)
109
+ other.is_a?(self.class) && other.to_h == self.to_h
110
+ end
111
+
112
+
113
+ def <=>(other)
114
+ self.to_h <=> other.to_h
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,86 @@
1
+ require 'trick_bag'
2
+ require 'ostruct'
3
+ require 'typhoeus'
4
+ require_relative 'get_helper'
5
+ require_relative 'params'
6
+
7
+
8
+ module SisRuby
9
+
10
+
11
+ # This class is an enumerator over a result set of SIS objects, as specified
12
+ # in a hash-like object (that is, any object that implements to_hash,
13
+ # such as a SisParams instance). It can be used as any Enumerable;
14
+ # that is, methods such as each, map, select, etc. can be called;
15
+ # an array of all results can be produced by calling to_a,
16
+ # and an Enumerator can be gotten by calling each without a chunk.
17
+ #
18
+ # Internally it fetches chunks of records only when needed to serve the values
19
+ # to the caller. The chunk size can be specified by the caller.
20
+ #
21
+ # By calling each or each_slice you can access some of the data before fetching
22
+ # all of it. This can be handy if:
23
+ #
24
+ # 1) there may not be enough available memory to process the entire result set
25
+ # at once
26
+ # 2) you want to process some records while others are being fetched, by
27
+ # using multiple threads, for example.
28
+ #
29
+ class ResultEnumerable < TrickBag::Enumerables::BufferedEnumerable
30
+
31
+ include GetHelper
32
+ include Enumerable
33
+
34
+ DEFAULT_CHUNK_RECORD_COUNT = 5_000
35
+
36
+
37
+ # @param endpoint_url the SIS endpoint URL string or an Endpoint instance
38
+ # @param params any object that implements to_hash, e.g. a Params instance
39
+ # @param chunk_size the maximum number of records to fetch from the server in a request
40
+ def initialize(endpoint_url, params = {}, chunk_size = DEFAULT_CHUNK_RECORD_COUNT)
41
+ super(chunk_size)
42
+ @endpoint_url = endpoint_url.is_a?(Endpoint) ? endpoint_url.url : endpoint_url
43
+ @outer_params = params.is_a?(Params) ? params : Params.from_hash(params.to_hash)
44
+
45
+ inner_limit = @outer_params.limit ? [chunk_size, @outer_params.limit].min : chunk_size
46
+ @inner_params = @outer_params.clone.limit(inner_limit)
47
+
48
+ @inner_params.offset ||= 0
49
+ @position = 0
50
+ @total_count = nil
51
+ end
52
+
53
+
54
+ def fetch
55
+ # This exits the fetching when the desired number of records has been fetched.
56
+ if @outer_params.limit && @yield_count >= @outer_params.limit
57
+ self.data = []
58
+ return
59
+ end
60
+
61
+ request = Typhoeus::Request.new(@endpoint_url, params: @inner_params.to_hash, headers: create_headers(true))
62
+ response = request.run
63
+ validate_response_success(response)
64
+ self.data = JSON.parse(response.body)
65
+ @total_count = response.headers['x-total-count'].to_i
66
+ @inner_params.offset(@inner_params.offset + chunk_size)
67
+
68
+ # TODO: Deal with this
69
+ # if @total_count > chunk_size && @inner_params.sort.nil?
70
+ # raise "Total count (#{@total_count}) exceeds chunk size (#{chunk_size})." +
71
+ # "When this is the case, a sort order must be specified, or chunk size increased."
72
+ # end
73
+ end
74
+
75
+
76
+ def total_count
77
+ fetch unless @total_count
78
+ @total_count
79
+ end
80
+
81
+
82
+ def fetch_notify
83
+ # puts "Fetch at #{Time.now}: chunk size: #{chunk_size}, yield count: #{yield_count}, total count = #{@total_count}"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ module SisRuby
2
+ VERSION = '1.0.0'
3
+ end
data/lib/sis_ruby.rb ADDED
@@ -0,0 +1,8 @@
1
+ # Load all *.rb files in lib/sis_ruby and below.
2
+ # Use a lambda so that the intermediate variables do not survive this file.
3
+ ->() {
4
+ start_dir = File.join(File.dirname(__FILE__), 'sis_ruby') # the lib directory
5
+ file_mask = "#{start_dir}/**/*.rb"
6
+ Dir[file_mask].each { |file| require file }
7
+ }.()
8
+
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sis_ruby'
4
+ require 'awesome_print'
5
+
6
+ include SisRuby
7
+
8
+ raise "Must specify SIS server URL in SIS_URL environment variable." unless ENV['SIS_URL']
9
+ client = Client.new(ENV['SIS_URL'])
10
+ host_endpoint = client.entities('host')
11
+
12
+ records = host_endpoint.list(Params.new.fields('hostname').limit(3))
13
+ puts "Fetched #{records.size} host records."
14
+ puts "Last one is: #{records.last}, host name is #{records.last['hostname']}"
15
+
16
+
17
+ full_record = host_endpoint.list(Params.new.limit(1)).first
18
+ puts 'Host record keys:'
19
+ ap full_record.keys
20
+
21
+ puts "\n\n"
22
+ query = Params.new.limit(1).fields('hostname', 'fqdn', 'status')
23
+ result = host_endpoint.list_as_openstructs(query).first
24
+ puts "Host: #{result.hostname}, FQDN: #{result.fqdn}, Status: #{result.status}"
25
+ puts "\nEntire record:"
26
+ ap result
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'awesome_print'
4
+ require 'pry'
5
+ require 'sis_ruby'
6
+
7
+ raise "Must specify SIS server URL in SIS_URL environment variable." unless ENV['SIS_URL']
8
+ client = Client.new(ENV['SIS_URL'])
9
+ HOSTS = client.entities('host')
10
+ # puts "Total host count is #{hosts.count}"
11
+ # puts "Total qa host count is #{hosts.count('environment' => 'qa')}"
12
+
13
+ # p1 = SisRuby::Params.new.sort('host').offset(9000).limit(2)
14
+ # p1 = SisRuby::Params.new.sort('host').offset(9000).limit(2).fields('hostname', 'environment')
15
+ # records = hosts.list(p1)
16
+ # ap records.first
17
+
18
+
19
+ def fetch(count, offset)
20
+ HOSTS.list(SisRuby::Params.new.fields('hostname').limit(count).offset(offset))
21
+ end
22
+
23
+ arrays = (0..10).to_a.map { |i| fetch(1000, i * 1000) }
24
+ a_combined = arrays.flatten
25
+ a_uniq = a_combined.uniq
26
+ host_names = a_combined.map { |h| h['hostname'] }
27
+
28
+ puts "Combined count is #{a_combined.size}"
29
+ puts "Unique count is #{a_uniq.size}"
30
+ puts "Host name count is #{host_names.size}"
31
+ puts "Unique host name count is #{host_names.uniq.size}"
32
+ binding.pry
33
+
data/sis_ruby.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ require File.join(File.dirname(__FILE__), 'lib', 'sis_ruby', 'version')
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'sis_ruby'
5
+ s.version = SisRuby::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ['Keith Bennett', 'Neel Goyal']
8
+ s.email = ['keithrbennett@gmail.com', 'sis-dev@verisign.com']
9
+ s.homepage = 'https://github.com/keithrbennett/sis_ruby'
10
+ s.summary = 'SIS Ruby Client'
11
+ s.description = 'Ruby client for SIS web service (https://github.com/sis-cmdb/sis-api)'
12
+ s.files = `git ls-files`.split($/)
13
+ s.require_path = 'lib'
14
+ s.license = 'BSD 3-Clause'
15
+
16
+ s.add_dependency 'awesome_print', '>= 1.6.0'
17
+ s.add_dependency 'rake'
18
+ s.add_dependency 'trick_bag', '>= 0.63.1'
19
+ s.add_dependency 'typhoeus', '>= 0.8.0'
20
+
21
+ s.add_development_dependency 'rspec', '>= 3.0'
22
+ end
@@ -0,0 +1,57 @@
1
+ require_relative 'spec_helper'
2
+ require_relative '../lib/sis_ruby/exceptions/missing_id_error'
3
+
4
+ # These tests test client behavior that does not require a server.
5
+
6
+ module SisRuby
7
+
8
+ describe Client do
9
+
10
+ let(:server_url) { 'https://fictitious-domain.com' }
11
+ let(:id_fieldname) { '_id_' }
12
+ let(:client) { Client.new(server_url) }
13
+ let(:host_endpoint) { client.entities('host', id_fieldname) }
14
+
15
+ context 'constructor parameters' do
16
+
17
+ specify 'omitting version gets default version' do
18
+ expect(client.api_version).to eq(Client::DEFAULT_API_VERSION)
19
+ end
20
+
21
+ specify 'overriding default version works' do
22
+ api_v = 'v_not_default'
23
+ expect(Client.new(server_url, api_version: api_v).api_version).to eq(api_v)
24
+ end
25
+
26
+ specify 'setting auth_token works' do
27
+ auth_token = 'abcdefgxyz'
28
+ expect(Client.new(server_url, auth_token: auth_token).auth_token).to eq(auth_token)
29
+ end
30
+
31
+ end
32
+
33
+ context 'precreated endpoints' do
34
+
35
+ specify 'are precreated' do
36
+ endpoints = [client.hooks, client.schemas, client.hiera]
37
+ expect(endpoints.all? { |ep| ep.is_a?(Endpoint) }).to eq(true)
38
+ end
39
+
40
+ specify 'have URLs that end in the right name' do
41
+ endpoints = [client.hooks, client.schemas, client.hiera]
42
+ endpoints_and_names = endpoints.zip(%w(hooks schemas hiera))
43
+ expect(endpoints_and_names.all? { |ep, name| ep.url.end_with?(name) }).to eq(true)
44
+ end
45
+ end
46
+
47
+
48
+ context 'tokens' do
49
+
50
+ specify 'the URL is correct' do
51
+ username = 'david_bisbal'
52
+ token = client.tokens(username)
53
+ expect(token.url.end_with?("users/#{username}/tokens")).to eq(true)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ require_relative 'spec_helper'
2
+ require_relative '../lib/sis_ruby/exceptions/missing_id_error'
3
+
4
+ module SisRuby
5
+
6
+ describe Endpoint do
7
+
8
+ let(:id_fieldname) { '_id_' }
9
+ let(:server_url) { 'https://fictitious-domain.com' }
10
+ let(:client) { Client.new(server_url) }
11
+ let(:host_endpoint) { client.entities('host', id_fieldname) }
12
+
13
+ context '#id_from_param' do
14
+
15
+ it 'works with an object other than hash' do
16
+ expect(host_endpoint.id_from_param(3)).to eq(3)
17
+ end
18
+
19
+ it 'works with a hash' do
20
+ expect(host_endpoint.id_from_param({ id_fieldname => 3 } )).to eq(3)
21
+ end
22
+
23
+ it 'raises an error on nil' do
24
+ expect { host_endpoint.id_from_param(nil) }.to raise_error(MissingIdError)
25
+ expect { host_endpoint.id_from_param( { id_fieldname => nil } ) }.to raise_error((MissingIdError))
26
+ end
27
+ end
28
+
29
+
30
+ context '#==' do
31
+
32
+ specify '2 endpoints created with the same parameters are ==' do
33
+ create_endpoint = -> { Endpoint.new(client, 'abcxyz', 'foo') }
34
+ endpoint_0 = create_endpoint.()
35
+ endpoint_1 = create_endpoint.()
36
+ expect(endpoint_1).to eq(endpoint_0)
37
+ end
38
+
39
+ specify '2 endpoints created with different clients are not ==' do
40
+ client1 = Client.new('url1')
41
+ client2 = Client.new('url2')
42
+ expect(Endpoint.new(client1, 'foo')).not_to eq(Endpoint.new(client2, 'foo'))
43
+ end
44
+
45
+ specify '2 endpoints created with different endpoint names are not ==' do
46
+ expect(Endpoint.new(client, 'foo')).not_to eq(Endpoint.new(client, 'bar'))
47
+ end
48
+
49
+ specify '2 endpoints created with different id fields are not ==' do
50
+ expect(Endpoint.new(client, 'foo', 'x')).not_to eq(Endpoint.new(client, 'foo', 'y'))
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,83 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe HashBuilder do
4
+
5
+ PERMITTED_ENTITY_ENDPOINT_TOP_LEVEL_KEYS = %w(
6
+ fields
7
+ id
8
+ limit
9
+ offset
10
+ q
11
+ )
12
+
13
+ QUERY_HASH = {
14
+ 'environment' => 'prod',
15
+ 'status' => 'live', # not maintenance
16
+ 'role.product.name' => 'mdns',
17
+ }
18
+
19
+ specify 'will create a limit key/value pair correctly' do
20
+ builder = HashBuilder.new
21
+ expect(builder.limit(10).to_h).to eq({ 'limit' => 10 })
22
+ end
23
+
24
+ specify 'subsequent calls will replace an existing value' do
25
+ builder = HashBuilder.new
26
+ builder.limit(10)
27
+ expect(builder.limit(11).to_h).to eq({ 'limit' => 11 })
28
+ end
29
+
30
+ specify 'a query hash will be correctly inserted' do
31
+ builder = HashBuilder.new
32
+ builder.q(QUERY_HASH)
33
+ expect(builder.to_h).to eq({ 'q' => QUERY_HASH})
34
+ end
35
+
36
+ specify '2 key/value pairs are no problem' do
37
+ builder = HashBuilder.new
38
+ builder.q(QUERY_HASH).limit(20)
39
+ expect(builder.to_h).to eq({ 'q' => QUERY_HASH, 'limit' => 20 })
40
+ end
41
+
42
+ specify 'keys are all Strings when key type of String is specified' do
43
+ builder = HashBuilder.new(String)
44
+ expect(builder.foo(1).bar(2).to_h.keys.all? { |key| key.is_a?(String) }).to eq(true)
45
+ end
46
+
47
+ specify 'keys are all Symbols when key type of Symbol is specified' do
48
+ builder = HashBuilder.new(Symbol)
49
+ expect(builder.foo(1).bar(2).to_h.keys.all? { |key| key.is_a?(Symbol) }).to eq(true)
50
+ end
51
+
52
+ specify 'respond_to_missing? always returns true' do
53
+ builder = HashBuilder.new
54
+ random_name = 'x' + Random.rand(1_000_000).to_s
55
+ expect(builder.respond_to?(random_name, false)).to eq(true)
56
+ end
57
+
58
+ # Disable permitted methods feature for now; it was not fully implemented,
59
+ # and should be a predicate lambda rather than an array anyway.
60
+
61
+ # context 'permitted methods' do
62
+ #
63
+ # specify 'when not specified, all are allowed' do
64
+ # builder = HashBuilder.new
65
+ # expect(builder.respond_to?('foo')).to eq(true)
66
+ # end
67
+ #
68
+ # specify 'when empty, none are allowed' do
69
+ # builder = HashBuilder.new(String, [])
70
+ # expect(builder.new(String).respond_to?('foo')).to eq(false)
71
+ # builder.something(123)
72
+ # expect { builder.something(123) }.to raise_error
73
+ # end
74
+ #
75
+ # specify 'when non-empty, only permitted are permitted' do
76
+ # builder = HashBuilder.new(String, [:foo])
77
+ # expect(builder.new(String).respond_to?('foo')).to eq(true)
78
+ # expect(builder.new(String).respond_to?('bar')).to eq(false)
79
+ #
80
+ # end
81
+ # end
82
+ end
83
+
@@ -0,0 +1,75 @@
1
+ require_relative 'spec_helper'
2
+
3
+ module SisRuby
4
+
5
+ describe Params do
6
+
7
+ it 'results in an empty hash when nothing is set' do
8
+ expect(Params.new.to_hash).to eq({})
9
+ end
10
+
11
+ it 'correctly concatenate fields' do
12
+ fields = %w(foo bar).to_set
13
+ params = Params.new.fields(fields)
14
+ expect(params.fields).to eq(fields)
15
+ expect(params.to_hash).to eq({ 'fields' => 'foo,bar'})
16
+ end
17
+
18
+ it 'sets limit correctly' do
19
+ params = Params.new.limit(3)
20
+ expect(params.limit).to eq(3)
21
+ expect(params.to_hash).to eq({ 'limit' => 3 })
22
+ end
23
+
24
+ it 'sets offset correctly' do
25
+ params = Params.new.offset(4)
26
+ expect(params.offset).to eq(4)
27
+ expect(params.to_hash).to eq({ 'offset' => 4 })
28
+ end
29
+
30
+ it 'sets filter correctly' do
31
+ filter = { 'my_numeric_field' => { '$gt' => 10 }}
32
+ params = Params.new.filter(filter)
33
+ expect(params.filter).to eq(filter)
34
+ expect(params.to_hash).to eq({ 'q' => filter })
35
+ end
36
+
37
+ it 'sets sort correctly' do
38
+ params = Params.new.sort('-count')
39
+ expect(params.sort).to eq('-count')
40
+ expect(params.to_hash).to eq({ 'sort' => '-count' })
41
+ end
42
+
43
+ specify 'to_h and to_hash return equal hashes' do
44
+ params = Params.new.sort('-count')
45
+ expect(params.to_h).to eq(params.to_hash)
46
+ end
47
+
48
+ specify '2 identical objects are ==, and <=> returns 0' do
49
+ params1 = Params.new.sort('-count')
50
+ params2 = Params.new.sort('-count')
51
+ expect(params1).to eq(params2)
52
+ expect(params1 <=> params2).to eq(0)
53
+ end
54
+
55
+ specify '2 different objects are not ==, and <=> returns non-0' do
56
+ params1 = Params.new.sort('-count')
57
+ params2 = Params.new.sort('-date')
58
+ expect(params1).not_to eq(params2)
59
+ expect(params1 <=> params2).not_to eq(0)
60
+ end
61
+
62
+
63
+ specify 'clone returns an equal copy' do
64
+ params_orig = Params.new.sort('-count').limit(300).filter({key: 'value'}).offset(100).fields('a', 'b')
65
+ params_clone = params_orig.clone
66
+ expect(params_clone).to eq(params_orig)
67
+ end
68
+
69
+ specify 'from_hash returns an equal copy' do
70
+ params_orig = Params.new.sort('-count').limit(300).filter({key: 'value'}).offset(100).fields('a', 'b')
71
+ params_copy = Params.from_hash(params_orig.to_hash)
72
+ expect(params_copy).to eq(params_orig)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ /* Drops the 'sis' data base. */
2
+ conn = new Mongo();
3
+ db = conn.getDB('sis')
4
+ db.dropDatabase();
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This script drops the Mongo DB and adds the test user.
4
+ # It assumes the existence of a ~/opt/sis-api directory
5
+ # that is a clone of the sis-api git repo, and a 'test-user.json'
6
+ # data file that is a copy of the file by that name in this directory.
7
+ #
8
+ # The script should be run from the directory in which it exists.
9
+
10
+ mongo drop-db.js
11
+ #echo "use sis;\n db.dropDatabase();\n exit" | mongo
12
+ cd ~/opt/sis-api
13
+ node tools/useradmin.js update test-user.json
14
+ cd -
@@ -0,0 +1,6 @@
1
+ {
2
+ "name" : "test",
3
+ "pw" : "abc123",
4
+ "email" : "nobody@bbs-software.com",
5
+ "super_user" : true
6
+ }
@@ -0,0 +1 @@
1
+ require_relative '../lib/sis_ruby'
@@ -0,0 +1,50 @@
1
+ {
2
+ "entity_type": "entity_test",
3
+ "id_field": "_id",
4
+ "required_schema": {
5
+ "name": "entity_test",
6
+ "owner": "schema_owner",
7
+ "definition": {
8
+ "name": {
9
+ "type": "String",
10
+ "required": true,
11
+ "unique": true
12
+ },
13
+ "number": {
14
+ "type": "Number",
15
+ "unique": true,
16
+ "required": true
17
+ }
18
+ },
19
+ "_sis": {
20
+ "owner": "test"
21
+ }
22
+ },
23
+ "valid_items": [
24
+ {
25
+ "name": "entity1",
26
+ "number": 0
27
+ },
28
+ {
29
+ "name": "entity2",
30
+ "number": 1
31
+ },
32
+ {
33
+ "name": "entity3",
34
+ "number": 2
35
+ }
36
+ ],
37
+ "invalid_items": [
38
+ {
39
+ "foo": "bar"
40
+ },
41
+ {
42
+ "name": "entity1",
43
+ "number": 4
44
+ },
45
+ {
46
+ "name": "non_unique_num",
47
+ "number": 0
48
+ }
49
+ ]
50
+ }