agentless-catalog-executor 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.dependency_decisions.yml +118 -0
  3. data/.gitignore +15 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +97 -0
  6. data/.travis.yml +7 -0
  7. data/CHANGELOG.md +44 -0
  8. data/Dockerfile +42 -0
  9. data/Gemfile +22 -0
  10. data/LICENSE +201 -0
  11. data/README.md +23 -0
  12. data/Rakefile +52 -0
  13. data/acceptance/.gitignore +10 -0
  14. data/acceptance/Gemfile +28 -0
  15. data/acceptance/Rakefile +16 -0
  16. data/acceptance/config/gem/options.rb +6 -0
  17. data/acceptance/config/git/options.rb +7 -0
  18. data/acceptance/config/package/options.rb +6 -0
  19. data/acceptance/lib/acceptance/ace_command_helper.rb +49 -0
  20. data/acceptance/lib/acceptance/ace_setup_helper.rb +45 -0
  21. data/acceptance/setup/common/pre-suite/.gitkeep +0 -0
  22. data/acceptance/setup/gem/pre-suite/.gitkeep +0 -0
  23. data/acceptance/setup/git/pre-suite/.gitkeep +0 -0
  24. data/acceptance/setup/package/pre-suite/.gitkeep +0 -0
  25. data/acceptance/tests/.gitkeep +0 -0
  26. data/agentless-catalog-executor.gemspec +38 -0
  27. data/config/docker.conf +9 -0
  28. data/config/local.conf +9 -0
  29. data/config/transport_tasks_config.rb +49 -0
  30. data/developer-docs/api.md +139 -0
  31. data/developer-docs/docker.md +170 -0
  32. data/docker-compose.yml +12 -0
  33. data/lib/ace/config.rb +73 -0
  34. data/lib/ace/configurer.rb +17 -0
  35. data/lib/ace/error.rb +31 -0
  36. data/lib/ace/fork_util.rb +39 -0
  37. data/lib/ace/plugin_cache.rb +79 -0
  38. data/lib/ace/puppet_util.rb +57 -0
  39. data/lib/ace/schemas/ace-execute_catalog.json +48 -0
  40. data/lib/ace/schemas/ace-run_task.json +30 -0
  41. data/lib/ace/schemas/task.json +74 -0
  42. data/lib/ace/transport_app.rb +252 -0
  43. data/lib/ace/version.rb +5 -0
  44. data/lib/puppet/indirector/catalog/certless.rb +83 -0
  45. metadata +254 -0
@@ -0,0 +1,170 @@
1
+ # Docker Docs
2
+
3
+ ## Setup
4
+
5
+ [Docker-compose installation](https://docs.docker.com/compose/install/) would need to be followed and setup in order to use the ACE containers for development.
6
+
7
+
8
+ The ACE compose file is dependent on a Docker network created when launching the Puppetserver and PuppetDB containers within the `spec/` directory, the network will default to `spec_default`, since the containers are built from the `spec/` directory they will be assigned the `<folder>_default` network.
9
+
10
+ As the ACE container is build outside of the `spec/` directory it would not be able to create the `spec_default` network. This can be created manually through:
11
+
12
+ ```
13
+ docker network create spec_default
14
+ ```
15
+
16
+ Once this is done the ACE container can be launched by executing the following within the root folder:
17
+
18
+ ```
19
+ docker-compose up -d --build
20
+ ```
21
+
22
+ This will take some time as it needs to perform the initial build of fetching the images and running through the build.
23
+
24
+ Navigate to the `spec/` folder and build the Puppetserver and PuppetDB containers using the same command. The Puppetserver will take some time to start and typically using the following command to verify that it is ready:
25
+
26
+ ```
27
+ docker logs --follow spec_puppetserver_1
28
+ ```
29
+
30
+ Once the Puppetserver is ready, the following message is reported:
31
+
32
+ ```
33
+ 2019-03-18 15:42:19,964 INFO [p.s.m.master-service] Puppet Server has successfully started and is now ready to handle requests
34
+ 2019-03-18 15:42:19,965 INFO [p.s.l.legacy-routes-service] The legacy routing service has successfully started and is now ready to handle requests
35
+ ```
36
+
37
+ On Linux, ensure that you have access to all volumes:
38
+
39
+ ```
40
+ sudo chmod a+rx -R volumes/
41
+ ```
42
+
43
+ At this point it is required to generate certs for the `aceserver`, this can be achieved though:
44
+
45
+ `docker exec spec_puppet_1 puppetserver ca generate --certname aceserver --subject-alt-names localhost,aceserver,ace_aceserver_1,spec_puppetserver_1,ace_server,puppet_server,spec_aceserver_1,puppetdb,spec_puppetdb_1,0.0.0.0,puppet`
46
+
47
+ On Linux, ensure that you have access to the newly created files:
48
+
49
+ ```
50
+ sudo chmod a+rx -R volumes/
51
+ ```
52
+
53
+ Reasoning for this is that it makes it easier to ensure that the cert names are consistent across environments.
54
+
55
+ ## Verifying the services
56
+
57
+ [Postman](https://www.getpostman.com/) is advisable to verify that the endpoints are configured. In order to set up Postman, navigate to Settings > Certificates and add client certificates for hosts `0.0.0.0:8140` and `0.0.0.0:44633` where the CRT file points to `spec/volumes/puppet/ssl/certs/aceserver.pem` and Key file points to `spec/volumes/puppet/ssl/private_keys/aceserver.pem`
58
+
59
+ *Note*: These cert and key files will only be created when the PuppetServer container has finished initalising.
60
+
61
+ ### PuppetServer /tasks/:module/:task
62
+
63
+ ```
64
+ https://0.0.0.0:8140/puppet/v3/tasks/:module/:task?environment=production
65
+ ```
66
+
67
+ Is the endpoint to get the task metadata from a PuppetServer, i.e.
68
+
69
+ ```
70
+ GET https://0.0.0.0:8140/puppet/v3/tasks/panos/apikey?environment=production
71
+
72
+ RESPONSE
73
+ {
74
+ "metadata": {
75
+ "description": "Retrieve a PAN-OS apikey",
76
+ "files": [
77
+ ...
78
+ ],
79
+ "parameters": {},
80
+ "puppet_task_version": 1,
81
+ "remote": true,
82
+ "supports_noop": false
83
+ },
84
+ "name": "panos::apikey",
85
+ "files": [
86
+ ...
87
+ ]
88
+ }
89
+ ```
90
+
91
+ This can be used to construct the request body that will be used to execute the [ACE `/run_task`](#ace-runtask) endpoint.
92
+
93
+ ### ACE /run_task
94
+
95
+ ```
96
+ POST https://0.0.0.0:44633/run_task
97
+ BODY {
98
+ "target": {
99
+ "remote-transport": "panos",
100
+ "name":"pavm",
101
+ "hostname": "vvtzckq3vzx995w.delivery.puppetlabs.net",
102
+ "user": "admin",
103
+ "password": "admin",
104
+ "ssl": false
105
+ },
106
+ "task": {
107
+ "metadata": {
108
+ "description": "Retrieve a PAN-OS apikey",
109
+ "files": [
110
+ ...
111
+ ],
112
+ "parameters": {},
113
+ "puppet_task_version": 1,
114
+ "remote": true,
115
+ "supports_noop": false
116
+ },
117
+ "name": "panos::apikey",
118
+ "files": [
119
+ ...
120
+ ]
121
+ }}
122
+
123
+ RESPONSE
124
+ {
125
+ "node": "vvtzckq3vzx995w.delivery.puppetlabs.net",
126
+ "status": "success",
127
+ "result": {
128
+ "apikey": "LUFRPT14MW5xOEo1R09KVlBZNnpnemh0VHRBOWl6TGM9bXcwM3JHUGVhRlNiY0dCR0srNERUQT09"
129
+ }
130
+ }
131
+ ```
132
+
133
+ Running the containers through Docker does have the benefit that the containers will be a better representation of how the ACE service will work in PE, however for developing and verifying changes it can be considered slow as changes may require the ACE container to be rebuilt which can take some time, an alternative approach for local development is [running the service locally](#running-ace-locally), this way the Puppetserver and PuppetDB containers are only required to be running.
134
+
135
+ ## Running ACE locally
136
+
137
+ The ACE service can also be ran directly through Puma rather than building the container, this has the benefits of being able to specifying local changes of Bolt within the Gemfile rather than having to make the changes in the container, or committing the changes and rebuilding the containers which can take some time.
138
+
139
+ When running locally through Puma there is a caveat on the tasks that are being executed and a possible conflict with the version of Ruby on the local installation, where solutions are highlighted in the [incorrect Puppet Ruby version](#incorrect-puppet-ruby-version).
140
+
141
+ Launching the service locally can be achieved by running the following:
142
+
143
+ ```
144
+ ACE_CONF=config/local.conf bundle exec puma -C config/transport_tasks_config.rb
145
+ ```
146
+
147
+ ### Incorrect Puppet Ruby version
148
+
149
+ The tasks typically used in networking modules have a shebang referencing the Puppet Ruby, there are two approaches to getting around this.
150
+
151
+ When constructing requests to be sent to ACE, in the target hash the interpreter can be specified, i.e.
152
+
153
+ ```
154
+ {
155
+ "target":{
156
+ "remote-transport":"panos",
157
+ "name":"pavm",
158
+ "interpreters":{
159
+ ".rb":"/Users/thomas.franklin/.rbenv/versions/2.5.1/bin/ruby"
160
+ }
161
+ },
162
+ "task": {hash from puppetserver /tasks endpoint}
163
+ }
164
+ ```
165
+
166
+ Although this would need to be included in every request - a 'permanent' solution would be to symlink the Puppet Ruby to the development version of Ruby, i.e.
167
+
168
+ ```
169
+ sudo ln -svf $(bundle exec which ruby) /opt/puppetlabs/puppet/bin/ruby
170
+ ```
@@ -0,0 +1,12 @@
1
+ version: "3"
2
+ services:
3
+ aceserver:
4
+ build: .
5
+ ports:
6
+ - "44633:44633"
7
+ tty: true
8
+ stdin_open: true
9
+ networks:
10
+ default:
11
+ external:
12
+ name: spec_default
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hocon'
4
+ require 'bolt_server/base_config'
5
+
6
+ module ACE
7
+ class Config < BoltServer::BaseConfig
8
+ attr_reader :data
9
+ def config_keys
10
+ super + %w[concurrency cache-dir puppet-server-conn-timeout puppet-server-uri ssl-ca-crls]
11
+ end
12
+
13
+ def env_keys
14
+ super + %w[concurrency puppet-server-conn-timeout puppet-server-uri ssl-ca-crls]
15
+ end
16
+
17
+ def ssl_keys
18
+ super + %w[ssl-ca-crls]
19
+ end
20
+
21
+ def int_keys
22
+ %w[concurrency puppet-server-conn-timeout]
23
+ end
24
+
25
+ def defaults
26
+ super.merge(
27
+ 'port' => 44633,
28
+ 'concurrency' => 10,
29
+ 'cache-dir' => "/opt/puppetlabs/server/data/ace-server/cache",
30
+ 'puppet-server-conn-timeout' => 120,
31
+ 'file-server-conn-timeout' => 120
32
+ )
33
+ end
34
+
35
+ def required_keys
36
+ super + %w[puppet-server-uri cache-dir]
37
+ end
38
+
39
+ def service_name
40
+ 'ace-server'
41
+ end
42
+
43
+ def load_env_config
44
+ env_keys.each do |key|
45
+ transformed_key = "ACE_#{key.tr('-', '_').upcase}"
46
+ next unless ENV.key?(transformed_key)
47
+ @data[key] = if int_keys.include?(key)
48
+ ENV[transformed_key].to_i
49
+ else
50
+ ENV[transformed_key]
51
+ end
52
+ end
53
+ end
54
+
55
+ def validate
56
+ super
57
+
58
+ unless natural?(@data['concurrency'])
59
+ raise Bolt::ValidationError, "Configured 'concurrency' must be a positive integer"
60
+ end
61
+
62
+ unless natural?(@data['puppet-server-conn-timeout'])
63
+ raise Bolt::ValidationError, "Configured 'puppet-server-conn-timeout' must be a positive integer"
64
+ end
65
+ end
66
+
67
+ def make_compatible
68
+ # This function sets values used by Bolt that behave the same in ACE, but have a different meaning
69
+ @data['file-server-uri'] = @data['puppet-server-uri']
70
+ @data['file-server-conn-timeout'] = @data['puppet-server-conn-timeout']
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet/configurer'
4
+
5
+ module ACE
6
+ class Configurer < Puppet::Configurer
7
+ # override the configurer to return the facts
8
+ # related to the transport and the trusted
9
+ # facts which is passed to the configurer.run
10
+ def get_facts(options)
11
+ transport_facts = Puppet::Node::Facts.indirection.find(Puppet[:certname],
12
+ environment: Puppet[:environment]).values
13
+ trusted_facts = options[:trusted_facts]
14
+ { transport_facts: transport_facts, trusted_facts: trusted_facts }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACE
4
+ class Error < RuntimeError
5
+ attr_reader :kind, :details, :issue_code, :error_code
6
+
7
+ def initialize(msg, kind, details = nil, issue_code = nil)
8
+ super(msg)
9
+ @kind = kind
10
+ @issue_code = issue_code
11
+ @details = details || {}
12
+ @error_code ||= 1
13
+ end
14
+
15
+ def msg
16
+ message
17
+ end
18
+
19
+ def to_h
20
+ h = { 'kind' => kind,
21
+ 'msg' => message,
22
+ 'details' => details }
23
+ h['issue_code'] = issue_code if issue_code
24
+ h
25
+ end
26
+
27
+ def to_json(opts = nil)
28
+ to_h.to_json(opts)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # English module required for $CHILD_STATUS rather than $?
4
+ require 'English'
5
+
6
+ module ACE
7
+ class ForkUtil
8
+ # Forks and calls a function
9
+ # It is expected that the function returns a JSON response
10
+ # Throws an exception if JSON.generate fails to generate
11
+ def self.isolate
12
+ reader, writer = IO.pipe
13
+ pid = fork {
14
+ success = true
15
+ begin
16
+ response = yield
17
+ writer.puts JSON.generate(response)
18
+ rescue StandardError => e
19
+ writer.puts(e)
20
+ success = false
21
+ ensure
22
+ Process.exit! success
23
+ end
24
+ }
25
+ unless pid
26
+ log "Could not fork"
27
+ exit 1
28
+ end
29
+ writer.close
30
+ output = reader.read
31
+ Process.wait(pid)
32
+ if $CHILD_STATUS != 0
33
+ raise output
34
+ else
35
+ JSON.parse(output)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'ace/puppet_util'
5
+ require 'puppet/configurer'
6
+ require 'concurrent'
7
+ require 'ace/fork_util'
8
+
9
+ module ACE
10
+ class PluginCache
11
+ attr_reader :cache_dir_mutex, :cache_dir
12
+ def initialize(environments_cache_dir)
13
+ @cache_dir = environments_cache_dir
14
+ @cache_dir_mutex = Concurrent::ReadWriteLock.new
15
+ end
16
+
17
+ def setup
18
+ FileUtils.mkdir_p(cache_dir)
19
+ self
20
+ end
21
+
22
+ def with_synced_libdir(environment, certname, &block)
23
+ ForkUtil.isolate do
24
+ ACE::PuppetUtil.isolated_puppet_settings(certname, environment)
25
+ with_synced_libdir_core(environment, &block)
26
+ end
27
+ end
28
+
29
+ def with_synced_libdir_core(environment)
30
+ pool = Puppet::Network::HTTP::Pool.new(Puppet[:http_keepalive_timeout])
31
+ Puppet.push_context({
32
+ http_pool: pool
33
+ }, "Isolated HTTP Pool")
34
+ libdir = sync_core(environment)
35
+ Puppet.settings[:libdir] = libdir
36
+ $LOAD_PATH << libdir
37
+ yield
38
+ ensure
39
+ FileUtils.remove_dir(libdir)
40
+ pool.close
41
+ end
42
+
43
+ # the Puppet[:libdir] will point to a tmp location
44
+ # where the contents from the pluginsync dest is copied
45
+ # too.
46
+ def libdir(plugin_dest)
47
+ tmpdir = Dir.mktmpdir(['plugins', plugin_dest])
48
+ cache_dir_mutex.with_write_lock do
49
+ FileUtils.cp_r(File.join(plugin_dest, '.'), tmpdir)
50
+ FileUtils.touch(tmpdir)
51
+ end
52
+ tmpdir
53
+ end
54
+
55
+ def environment_dir(environment)
56
+ environment_dir = File.join(cache_dir, environment)
57
+ cache_dir_mutex.with_write_lock do
58
+ FileUtils.mkdir_p(environment_dir)
59
+ FileUtils.touch(environment_dir)
60
+ end
61
+ environment_dir
62
+ end
63
+
64
+ # @returns the tmp libdir directory which will be where
65
+ # Puppet[:libdir] is referenced too
66
+ def sync_core(environment)
67
+ env = Puppet::Node::Environment.remote(environment)
68
+ environments_dir = environment_dir(environment)
69
+ Puppet[:vardir] = File.join(environments_dir)
70
+ Puppet[:confdir] = File.join(environments_dir, 'conf')
71
+ Puppet[:rundir] = File.join(environments_dir, 'run')
72
+ Puppet[:logdir] = File.join(environments_dir, 'log')
73
+ Puppet[:codedir] = File.join(environments_dir, 'code')
74
+ Puppet[:plugindest] = File.join(environments_dir, 'plugins')
75
+ Puppet::Configurer::PluginHandler.new.download_plugins(env)
76
+ libdir(File.join(environments_dir, 'plugins'))
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module ACE
6
+ class PuppetUtil
7
+ def self.init_global_settings(ca_cert_path, ca_crls_path, private_key_path, client_cert_path, cachedir, uri)
8
+ Puppet::Util::Log.destinations.clear
9
+ Puppet::Util::Log.newdestination(:console)
10
+ Puppet.settings[:log_level] = 'notice'
11
+ Puppet.settings[:trace] = true
12
+ Puppet.settings[:catalog_terminus] = :certless
13
+ Puppet.settings[:node_terminus] = :memory
14
+ Puppet.settings[:catalog_cache_terminus] = :json
15
+ Puppet.settings[:facts_terminus] = :network_device
16
+ # the following settings are just to make base_context
17
+ # happy, these will not be the final values,
18
+ # as per request settings will be set later on
19
+ # to satisfy multi-environments
20
+ Puppet.settings[:vardir] = cachedir
21
+ Puppet.settings[:confdir] = File.join(cachedir, 'conf')
22
+ Puppet.settings[:rundir] = File.join(cachedir, 'run')
23
+ Puppet.settings[:logdir] = File.join(cachedir, 'log')
24
+ Puppet.settings[:codedir] = File.join(cachedir, 'code')
25
+ Puppet.settings[:plugindest] = File.join(cachedir, 'plugins')
26
+ Puppet.push_context(Puppet.base_context(Puppet.settings), "Puppet Initialization")
27
+ # ssl_context will be a persistent context
28
+ cert_provider = Puppet::X509::CertProvider.new(
29
+ capath: ca_cert_path,
30
+ crlpath: ca_crls_path
31
+ )
32
+ ssl_context = Puppet::SSL::SSLProvider.new.create_context(
33
+ cacerts: cert_provider.load_cacerts(required: true),
34
+ crls: cert_provider.load_crls(required: true),
35
+ private_key: OpenSSL::PKey::RSA.new(File.read(private_key_path, encoding: 'utf-8')),
36
+ client_cert: OpenSSL::X509::Certificate.new(File.read(client_cert_path, encoding: 'utf-8'))
37
+ )
38
+ Puppet.push_context({
39
+ ssl_context: ssl_context,
40
+ server: uri.host,
41
+ serverport: uri.port
42
+ }, "PuppetServer connection information to be used")
43
+ Puppet.settings.use :main, :agent, :ssl
44
+ Puppet::Transaction::Report.indirection.terminus_class = :rest
45
+ end
46
+
47
+ def self.isolated_puppet_settings(certname, environment)
48
+ Puppet.settings[:certname] = certname
49
+ Puppet.settings[:environment] = environment
50
+ env = Puppet::Node::Environment.remote(environment)
51
+ Puppet.push_context({
52
+ configured_environment: environment,
53
+ loaders: Puppet::Pops::Loaders.new(env)
54
+ }, "Isolated settings to be used")
55
+ end
56
+ end
57
+ end