sis_ruby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }