scooter 0.0.0 → 3.2.19

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.
Files changed (47) hide show
  1. checksums.yaml +15 -0
  2. data/.env +5 -0
  3. data/.gitignore +47 -19
  4. data/Gemfile +3 -0
  5. data/HISTORY.md +1539 -0
  6. data/README.md +69 -10
  7. data/Rakefile +7 -0
  8. data/docs/http_dispatchers.md +79 -0
  9. data/lib/scooter.rb +11 -3
  10. data/lib/scooter/httpdispatchers.rb +12 -0
  11. data/lib/scooter/httpdispatchers/activity.rb +46 -0
  12. data/lib/scooter/httpdispatchers/activity/v1/v1.rb +50 -0
  13. data/lib/scooter/httpdispatchers/classifier.rb +376 -0
  14. data/lib/scooter/httpdispatchers/classifier/v1/v1.rb +99 -0
  15. data/lib/scooter/httpdispatchers/code_manager.rb +31 -0
  16. data/lib/scooter/httpdispatchers/code_manager/v1/v1.rb +17 -0
  17. data/lib/scooter/httpdispatchers/consoledispatcher.rb +132 -0
  18. data/lib/scooter/httpdispatchers/httpdispatcher.rb +168 -0
  19. data/lib/scooter/httpdispatchers/orchestrator/v1/v1.rb +87 -0
  20. data/lib/scooter/httpdispatchers/orchestratordispatcher.rb +83 -0
  21. data/lib/scooter/httpdispatchers/puppetdb/v4/v4.rb +51 -0
  22. data/lib/scooter/httpdispatchers/puppetdbdispatcher.rb +390 -0
  23. data/lib/scooter/httpdispatchers/rbac.rb +231 -0
  24. data/lib/scooter/httpdispatchers/rbac/v1/directory_service.rb +68 -0
  25. data/lib/scooter/httpdispatchers/rbac/v1/v1.rb +116 -0
  26. data/lib/scooter/ldap.rb +349 -0
  27. data/lib/scooter/ldap/ldap_fixtures.rb +60 -0
  28. data/lib/scooter/middleware/rbac_auth_token.rb +35 -0
  29. data/lib/scooter/utilities.rb +9 -0
  30. data/lib/scooter/utilities/beaker_utilities.rb +41 -0
  31. data/lib/scooter/utilities/string_utilities.rb +32 -0
  32. data/lib/scooter/version.rb +3 -1
  33. data/scooter.gemspec +23 -6
  34. data/spec/scooter/beaker_utilities_spec.rb +53 -0
  35. data/spec/scooter/httpdispatchers/activity/activity_spec.rb +218 -0
  36. data/spec/scooter/httpdispatchers/classifier/classifier_spec.rb +542 -0
  37. data/spec/scooter/httpdispatchers/code_manager/code-manager_spec.rb +67 -0
  38. data/spec/scooter/httpdispatchers/consoledispatcher_spec.rb +80 -0
  39. data/spec/scooter/httpdispatchers/httpdispatcher_spec.rb +91 -0
  40. data/spec/scooter/httpdispatchers/middleware/rbac_auth_token_spec.rb +58 -0
  41. data/spec/scooter/httpdispatchers/orchestratordispatcher_spec.rb +195 -0
  42. data/spec/scooter/httpdispatchers/puppetdbdispatcher_spec.rb +246 -0
  43. data/spec/scooter/httpdispatchers/rbac/rbac_spec.rb +387 -0
  44. data/spec/scooter/string_utilities_spec.rb +83 -0
  45. data/spec/spec_helper.rb +8 -0
  46. metadata +270 -18
  47. data/LICENSE.txt +0 -15
@@ -0,0 +1,99 @@
1
+ module Scooter
2
+ module HttpDispatchers
3
+ module Classifier
4
+ # Methods here are generally representative of endpoints, and depending
5
+ # on the method, return either a Faraday response object or some sort of
6
+ # instance of the object created/modified.
7
+ module V1
8
+
9
+ def create_node_group(options)
10
+ set_classifier_path
11
+ if options['id']
12
+ @connection.put("v1/groups/#{options['id']}") do |request|
13
+ request.body = options
14
+ end
15
+ else
16
+ @connection.post('v1/groups') do |request|
17
+ request.body = options
18
+ end
19
+ end
20
+ end
21
+
22
+ def get_list_of_node_groups
23
+ set_classifier_path
24
+ @connection.get('v1/groups').env.body
25
+ end
26
+
27
+ def get_node_group(uuid)
28
+ set_classifier_path
29
+ @connection.get("v1/groups/#{uuid}").env.body
30
+ end
31
+
32
+ def delete_node_group(uuid)
33
+ set_classifier_path
34
+ @connection.delete("v1/groups/#{uuid}")
35
+ end
36
+
37
+ def replace_node_group(node_group_id, node_group_model)
38
+ set_classifier_path
39
+ @connection.put("v1/groups/#{node_group_id}") do |request|
40
+ request.body = node_group_model
41
+ end
42
+ end
43
+
44
+ def update_node_group(node_group_id, update_hash)
45
+ set_classifier_path
46
+ @connection.post("v1/groups/#{node_group_id}") do |request|
47
+ request.body = update_hash
48
+ end
49
+ end
50
+
51
+ def import_hierarchy(hierarchy)
52
+ set_classifier_path
53
+ @connection.post('v1/import-hierarchy') do |request|
54
+ request.body = hierarchy
55
+ end
56
+ end
57
+
58
+ def update_classes(environment=nil)
59
+ set_classifier_path
60
+ @connection.post('v1/update-classes') do |request|
61
+ unless environment.nil?
62
+ request.params['environment'] = environment
63
+ end
64
+ end
65
+ end
66
+
67
+ def pin_nodes(node_group_id, nodes)
68
+ set_classifier_path
69
+ @connection.post("v1/groups/#{node_group_id}/pin") do |request|
70
+ request.body = nodes
71
+ end
72
+ end
73
+
74
+ def unpin_nodes(node_group_id, nodes)
75
+ set_classifier_path
76
+ @connection.post("v1/groups/#{node_group_id}/unpin") do |request|
77
+ request.body = nodes
78
+ end
79
+ end
80
+
81
+ def get_list_of_classes
82
+ set_classifier_path
83
+ @connection.get('v1/classes').env.body
84
+ end
85
+
86
+ def get_list_of_nodes
87
+ set_classifier_path
88
+ @connection.get('v1/nodes').env.body
89
+ end
90
+
91
+ def get_list_of_environments
92
+ set_classifier_path
93
+ @connection.get('v1/environments').env.body
94
+ end
95
+
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,31 @@
1
+ %w( v1 ).each do |lib|
2
+ require "scooter/httpdispatchers/code_manager/v1/#{lib}"
3
+ end
4
+
5
+ module Scooter
6
+ module HttpDispatchers
7
+ # Currently there is no httpdispatcher built for the CodeManager module
8
+ # it is necessary to create one and extend this module onto it
9
+ #
10
+ # Example:
11
+ # api = Scooter::HttpDispatchers::HttpDispatcher.new(master)
12
+ # api.extend(Scooter::HttpDispatchers::CodeManager)
13
+ # api.deploy_environments(['environment_one', 'environment_two'])
14
+ module CodeManager
15
+
16
+ include Scooter::HttpDispatchers::CodeManager::V1
17
+
18
+ def deploy_environments(environment_array, wait=true)
19
+ raise ArgumentError.new('wait must be TrueClass or FalseClass') unless !!wait == wait
20
+ payload = {:environments => environment_array, 'wait' => wait}
21
+ deploys(payload)
22
+ end
23
+
24
+ def deploy_all_environments(wait=true)
25
+ raise ArgumentError.new('wait must be TrueClass or FalseClass') unless !!wait == wait
26
+ payload = {'deploy-all' => true, 'wait' => wait}
27
+ deploys(payload)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module Scooter
2
+ module HttpDispatchers
3
+ module CodeManager
4
+ # Methods here are generally representative of endpoints
5
+ module V1
6
+
7
+ def deploys(environments_payload_hash)
8
+ @connection.url_prefix.port = 8170
9
+ @connection.post('/code-manager/v1/deploys') do |req|
10
+ req.body = environments_payload_hash
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,132 @@
1
+ %w( rbac classifier activity).each do |lib|
2
+ require "scooter/httpdispatchers/#{lib}"
3
+ end
4
+
5
+ module Scooter
6
+
7
+ module HttpDispatchers
8
+
9
+ # == Quick-start guide
10
+ # === example creating a ConsoleDispatcher with certificates and credentials
11
+ #
12
+ # require 'scooter'
13
+ #
14
+ # include Scooter::HttpDispatcher
15
+ #
16
+ # console_dispatcher = ConsoleDispatcher.new(dashboard)
17
+ #
18
+ # # creates a new user and returns a new ConsoleDispatcher
19
+ # new_user = console_dispatcher.generate_local_user('login' => 'Chuck')
20
+ #
21
+ # # because the new_user has credentials, it can signin
22
+ # new_user.signin
23
+ #
24
+ # # this will trigger the middleware error handler, because you cannot
25
+ # # create two users with the same login
26
+ # console_dispatcher.create_local_user('login' => 'Chuck')
27
+ #
28
+ # # this will return Chuck's data
29
+ # certificate_dispatcher.get_console_dispatcher_data(new_user)
30
+ #
31
+ # # this will trigger a middleware error handler, because Chuck has no
32
+ # # privilege to create new users
33
+ # new_user.generate_local_user
34
+ #
35
+ # # this will delete Chuck from RBAC
36
+ # certificate_dispatcher.delete_local_console_dispatcher(new_user)
37
+ #
38
+ # # this method will now return nil because new_user is deleted
39
+ # certificate_dispatcher.get_console_dispatcher_data(new_guy)
40
+ class ConsoleDispatcher < HttpDispatcher
41
+
42
+ include Scooter::HttpDispatchers::Rbac
43
+ include Scooter::HttpDispatchers::Classifier
44
+ include Scooter::HttpDispatchers::Activity
45
+ attr_accessor :credentials
46
+ Credentials = Struct.new(:login, :password)
47
+
48
+ # This class is designed to interact with any of the pe-console-services:
49
+ # RBAC, Node Classifier, and the Activity Service. The most common use
50
+ # case will be for tests executed with beaker, for which you will want
51
+ # to pass in the dashboard for initialization. The other parameter that
52
+ # you can pass in optionally is credentials, if you wish to get a web
53
+ # session and use that to talk to various other parts of PE.
54
+ #
55
+ # If you are using this class outside of beaker, you can initialize an
56
+ # object without any parameters and supply your own dashboard as a String,
57
+ # credentials, Faraday connection object, and ssl parameters. Note that
58
+ # this is largely untested. Use outside of a Beaker test run at your own
59
+ # risk.
60
+ #
61
+ # @param credentials(Hash optional) Provide credentials if you wish to
62
+ # communicate through the UI proxy.
63
+ def initialize(host, credentials=nil, log_level=Logger::DEBUG, log_body=true)
64
+ @credentials = Credentials.new(credentials[:login],
65
+ credentials[:password]) if credentials
66
+ super(host, log_level, log_body)
67
+ end
68
+
69
+ def set_url_prefix(connection=self.connection)
70
+ connection.url_prefix.port = 4433
71
+ super(connection)
72
+ end
73
+
74
+ # This is overridden from the parent class; in the case of
75
+ # ConsoleDispatcher objects, there are often cases where the Dispatcher is
76
+ # not a certificate dispatcher, but representing an LDAP or local user. If
77
+ # credentials are supplied during initialization, then this overrides the
78
+ # parent class and only acquires a ca_cert.
79
+ def acquire_ssl_components(host=self.host)
80
+ if credentials == nil
81
+ super(host)
82
+ else
83
+ if !host.is_a?(Unix::Host)
84
+ raise 'Can only acquire SSL certs if the host is a Unix::Host'
85
+ end
86
+ acquire_ca_cert(host)
87
+ end
88
+ end
89
+
90
+ def signin(login=self.credentials.login, password=self.credentials.password)
91
+ response = @connection.post "/auth/login" do |request|
92
+ request.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
93
+ request.body = "username=#{login}&password=#{CGI.escape(password)}"
94
+ connection.port = 443
95
+ end
96
+ #return the response if the status code was not 200
97
+ return response if response.status != 200
98
+ # try to be helpful and acquire the xcsrf; catch any error that occurs
99
+ # in the acquire_xcsrf method
100
+ begin
101
+ acquire_xcsrf
102
+ rescue
103
+ # do nothing in the rescue
104
+ end
105
+ # Reset the connection port, since we have to hardcode it to 443 signin
106
+ # here
107
+ set_url_prefix
108
+ end
109
+
110
+ def acquire_xcsrf
111
+ # This simply makes a call to the base_uri and extracts out an
112
+ # anti-forgery-token and adds that token to the headers for the
113
+ # connection object
114
+ response_body = @connection.get('/') { |req| connection.port = 443 }.env.body
115
+ parsed_body = Nokogiri::HTML(response_body)
116
+ token = parsed_body.css("meta[name='__anti-forgery-token']")[0].attributes['content'].value
117
+ @connection.headers['X-CSRF-Token'] = token
118
+ set_url_prefix
119
+ end
120
+
121
+ def reset_local_user_password(token, new_password)
122
+ @connection.post "https://#{host}/auth/reset" do |request|
123
+ request.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
124
+ request.body = "password=#{new_password}&token=#{token}"
125
+ connection.port = 443
126
+ end
127
+ set_url_prefix
128
+ end
129
+
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,168 @@
1
+ module Scooter
2
+ module HttpDispatchers
3
+
4
+ require 'scooter/httpdispatchers/code_manager'
5
+ require 'scooter/middleware/rbac_auth_token'
6
+
7
+ # <i>HttpDispatcher</i> is the base class to extend when constructing
8
+ # service specific objects. It contains specific logic to extract out
9
+ # certificates from Beaker hosts provided as the host argument.
10
+ # You can directly instantiate from this class if you would like only this
11
+ # low level functionality. Otherwise, the primary function of this class
12
+ # is to allow more specific Dispatchers, such as the ConsoleDispatcher, to
13
+ # extend it and write higher level functionality.
14
+ class HttpDispatcher
15
+
16
+
17
+ attr_accessor :connection, :host, :ssl, :token, :send_auth_token_as_query_param, :faraday_logger
18
+ # The only required parameter for the HttpDispatcher is the host, which
19
+ # could either be a beaker Unix::Host or a String. HttpDispatchers offer
20
+ # support for automatically generating the required SSL components for the
21
+ # Dispatcher if it is passed a Unix Host.
22
+ #
23
+ # If it is only passed a String, than it is up to the caller to correctly
24
+ # configure the connection object to be configured correctly. Support for
25
+ # Strings is experimental for now; it may be deprecated if there is no
26
+ # feedback indicating that this functionality is being used.
27
+ #
28
+ # @param host(Unix::Host) The beaker host object you wish to communicate
29
+ # with.
30
+ # @param log_level(Int) The desired log level
31
+ # @param log_body(Boolean) Whether to log the body of responses
32
+ def initialize(host, log_level=Logger::DEBUG, log_body=true)
33
+ @log_body = log_body
34
+ @ssl = {}
35
+ @host = host
36
+ if @host.is_a?(Unix::Host)
37
+ @connection = create_default_connection_with_beaker_host
38
+ elsif @host.is_a?(String)
39
+ @connection = create_default_connection(log_level)
40
+ set_url_prefix
41
+ else
42
+ raise "Argument host must be Unix::Host or String"
43
+ end
44
+ # The http-cookie library that the cookie-jar wraps requires that a
45
+ # URI object be specifically a URI::HTTPS object. This changes the
46
+ # default url_prefix in Faraday to be sub-classed from HTTPS, not plain
47
+ # old HTTP. This should have no effect on any other middleware, as HTTPS
48
+ # is just HTTP subclassed with different defaults.
49
+ @connection.url_prefix = URI.parse(@connection.url_prefix.to_s)
50
+ end
51
+
52
+ def initialize_connection
53
+ if @host.is_a?(Unix::Host)
54
+ @connection = create_default_connection_with_beaker_host
55
+ elsif @host.is_a?(String)
56
+ @connection = create_default_connection
57
+ set_url_prefix
58
+ add_ssl_components_to_connection
59
+ else
60
+ raise "Argument host must be Unix::Host or String"
61
+ end
62
+
63
+ end
64
+
65
+ def create_default_connection(log_level=Logger::DEBUG)
66
+ Faraday.new do |conn|
67
+ conn.request :rbac_auth_token, self
68
+ conn.request :json
69
+
70
+ conn.response :follow_redirects
71
+ conn.response :raise_error
72
+ conn.response :json, :content_type => /\bjson$/
73
+ @faraday_logger ||= Logger.new $stderr
74
+ # If log level is not set by Beaker, set faraday log level to debug.
75
+ @faraday_logger.level ||= log_level if defined? @faraday_logger.level
76
+ conn.response :logger, @faraday_logger, bodies: @log_body
77
+
78
+ conn.use :cookie_jar
79
+
80
+ conn.adapter :net_http
81
+ end
82
+ end
83
+
84
+ def set_url_prefix(connection=self.connection)
85
+ if host.is_a?(Unix::Host)
86
+ connection.url_prefix.host = is_resolvable ? host.hostname : Scooter::Utilities::BeakerUtilities.get_public_ip(host)
87
+ else
88
+ connection.url_prefix.host = host
89
+ end
90
+ end
91
+
92
+ def create_default_connection_with_beaker_host
93
+ if host.logger
94
+ log_level = Logger::ERROR if host.logger.log_level == :error
95
+ log_level = Logger.WARN if host.logger.log_level == :warn
96
+ log_level = Logger::INFO if (host.logger.log_level == :info || host.logger.log_level == :notify)
97
+ log_level ||= Logger::DEBUG
98
+ end
99
+
100
+ connection = create_default_connection(log_level)
101
+ set_url_prefix(connection)
102
+ acquire_ssl_components if ssl.empty?
103
+ add_ssl_components_to_connection(connection)
104
+ connection
105
+ end
106
+
107
+ # See if we can reach the host by hostname
108
+ def is_resolvable(host=self.host)
109
+ begin
110
+ Resolv.getaddress(host.hostname)
111
+ true
112
+ rescue Resolv::ResolvError
113
+ false
114
+ end
115
+ end
116
+
117
+ # If you would like to run tests that expect 400 or even 500 responses,
118
+ # apply this method to remove the <tt>:raise_error</tt> middleware.
119
+ def remove_error_checking(connection=self.connection)
120
+ connection.builder.delete(Faraday::Response::RaiseError)
121
+ end
122
+
123
+ def acquire_ssl_components(host=self.host)
124
+ if !host.is_a?(Unix::Host)
125
+ raise 'Can only acquire SSL certs if the host is a Unix::Host'
126
+ end
127
+ acquire_ca_cert(host)
128
+ acquire_cert_and_key(host)
129
+ end
130
+
131
+ def acquire_ca_cert(host=self.host)
132
+ @ssl['ca_file'] = Scooter::Utilities::BeakerUtilities.pe_ca_cert_file(host)
133
+ end
134
+
135
+ def acquire_cert_and_key(host=self.host)
136
+ client_key = Scooter::Utilities::BeakerUtilities.pe_private_key(host)
137
+ client_cert = Scooter::Utilities::BeakerUtilities.pe_hostcert(host)
138
+ @ssl['client_key'] = OpenSSL::PKey.read(client_key)
139
+ @ssl['client_cert'] = OpenSSL::X509::Certificate.new(client_cert)
140
+ end
141
+
142
+ def add_ssl_components_to_connection(connection=self.connection)
143
+ # return immediately if the ssl object is empty
144
+ if ssl.empty?
145
+ warn 'no ssl keys defined, the connection object will not be modified'
146
+ return
147
+ end
148
+ # enforce the scheme to be https, since we are adding ssl components to
149
+ # the connection object
150
+ connection.url_prefix.scheme = 'https'
151
+
152
+ @ssl.each do |k, v|
153
+ connection.ssl[k] = v
154
+ end
155
+
156
+ if host.is_a?(Unix::Host)
157
+ if connection.url_prefix.host == Scooter::Utilities::BeakerUtilities.get_public_ip(host) && connection.ssl['verify'] == nil
158
+ # Because we are connecting to the dashboard by IP address, SSL verification
159
+ # against the CA will fail. Disable verifying against it for now until a better
160
+ # fix can be found.
161
+ connection.ssl['verify'] = false
162
+ end
163
+ end
164
+ end
165
+
166
+ end
167
+ end
168
+ end