octocatalog-diff 0.5.1
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.
- checksums.yaml +7 -0
- data/.version +1 -0
- data/LICENSE +20 -0
- data/README.md +82 -0
- data/bin/octocatalog-diff +75 -0
- data/doc/advanced-bootstrap.md +33 -0
- data/doc/advanced-cache-dir.md +24 -0
- data/doc/advanced-catalog-only.md +37 -0
- data/doc/advanced-ci.md +13 -0
- data/doc/advanced-dynamic-ignores.md +123 -0
- data/doc/advanced-future-parser.md +11 -0
- data/doc/advanced-ignores.md +224 -0
- data/doc/advanced-output-formats.md +96 -0
- data/doc/advanced-output-hacks.md +45 -0
- data/doc/advanced-override-facts.md +67 -0
- data/doc/advanced-pe-enc.md +52 -0
- data/doc/advanced-puppet-master.md +50 -0
- data/doc/advanced-puppet-versions.md +9 -0
- data/doc/advanced-storeconfigs.md +72 -0
- data/doc/advanced-using-without-git.md +15 -0
- data/doc/advanced.md +43 -0
- data/doc/basic.md +70 -0
- data/doc/configuration-enc.md +69 -0
- data/doc/configuration-hiera.md +103 -0
- data/doc/configuration-puppetdb.md +49 -0
- data/doc/configuration.md +51 -0
- data/doc/dev/README.md +1 -0
- data/doc/dev/coverage.md +34 -0
- data/doc/dev/how-to-add-options.md +83 -0
- data/doc/dev/integration-tests.md +63 -0
- data/doc/dev/releasing.md +19 -0
- data/doc/installation.md +49 -0
- data/doc/limitations.md +34 -0
- data/doc/optionsref.md +947 -0
- data/doc/requirements.md +16 -0
- data/doc/roadmap.md +26 -0
- data/doc/similar.md +17 -0
- data/doc/troubleshooting.md +54 -0
- data/lib/octocatalog-diff.rb +12 -0
- data/lib/octocatalog-diff/bootstrap.rb +53 -0
- data/lib/octocatalog-diff/catalog-diff/cli.rb +205 -0
- data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +240 -0
- data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +145 -0
- data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +99 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options.rb +173 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/basedir.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_environment.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_script.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_then_exit.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrapped_dirs.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/cached_master_dir.rb +21 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/catalog_only.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/color.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/compare_file_text.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/debug.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_datatype_changes.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_detail_add.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/display_source_file_line.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/enc.rb +31 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/existing_catalogs.rb +25 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/fact_file.rb +23 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/fact_override.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/facts_terminus.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/from_puppetdb.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/header.rb +24 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_config.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_path_strip.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/hostname.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore.rb +24 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_attr.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_tags.rb +23 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/include_tags.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/master_cache_branch.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/output_file.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/output_format.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/parallel.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/parser.rb +48 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pass_env_vars.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_ca.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_cert.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_key.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token_file.rb +17 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_url.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_binary.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_api_version.rb +20 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_ca.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_cert.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_key.rb +19 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_ca.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_cert.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_key.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password_file.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_url.rb +18 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/quiet.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/retry_failed_catalog.rb +13 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/safe_to_delete_cached_master_dir.rb +15 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/storeconfigs.rb +12 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/suppress_absent_file_details.rb +14 -0
- data/lib/octocatalog-diff/catalog-diff/cli/options/to_from_branch.rb +16 -0
- data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +52 -0
- data/lib/octocatalog-diff/catalog-diff/differ.rb +615 -0
- data/lib/octocatalog-diff/catalog-diff/display.rb +125 -0
- data/lib/octocatalog-diff/catalog-diff/display/json.rb +25 -0
- data/lib/octocatalog-diff/catalog-diff/display/text.rb +452 -0
- data/lib/octocatalog-diff/catalog-util/bootstrap.rb +145 -0
- data/lib/octocatalog-diff/catalog-util/builddir.rb +289 -0
- data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +169 -0
- data/lib/octocatalog-diff/catalog-util/command.rb +96 -0
- data/lib/octocatalog-diff/catalog-util/enc.rb +77 -0
- data/lib/octocatalog-diff/catalog-util/enc/noop.rb +22 -0
- data/lib/octocatalog-diff/catalog-util/enc/pe.rb +99 -0
- data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +61 -0
- data/lib/octocatalog-diff/catalog-util/enc/script.rb +88 -0
- data/lib/octocatalog-diff/catalog-util/facts.rb +89 -0
- data/lib/octocatalog-diff/catalog-util/fileresources.rb +83 -0
- data/lib/octocatalog-diff/catalog-util/git.rb +65 -0
- data/lib/octocatalog-diff/catalog.rb +209 -0
- data/lib/octocatalog-diff/catalog/computed.rb +205 -0
- data/lib/octocatalog-diff/catalog/json.rb +30 -0
- data/lib/octocatalog-diff/catalog/noop.rb +19 -0
- data/lib/octocatalog-diff/catalog/puppetdb.rb +82 -0
- data/lib/octocatalog-diff/catalog/puppetmaster.rb +121 -0
- data/lib/octocatalog-diff/external/pson/LICENSE +17 -0
- data/lib/octocatalog-diff/external/pson/README.md +20 -0
- data/lib/octocatalog-diff/external/pson/common.rb +370 -0
- data/lib/octocatalog-diff/external/pson/pure.rb +15 -0
- data/lib/octocatalog-diff/external/pson/pure/generator.rb +395 -0
- data/lib/octocatalog-diff/external/pson/pure/parser.rb +307 -0
- data/lib/octocatalog-diff/external/pson/version.rb +8 -0
- data/lib/octocatalog-diff/facts.rb +125 -0
- data/lib/octocatalog-diff/facts/json.rb +20 -0
- data/lib/octocatalog-diff/facts/puppetdb.rb +59 -0
- data/lib/octocatalog-diff/facts/yaml.rb +29 -0
- data/lib/octocatalog-diff/puppetdb.rb +163 -0
- data/lib/octocatalog-diff/util/colored.rb +20 -0
- data/lib/octocatalog-diff/util/httparty.rb +158 -0
- data/lib/octocatalog-diff/util/parallel.rb +170 -0
- data/lib/octocatalog-diff/util/puppetversion.rb +24 -0
- data/lib/octocatalog-diff/version.rb +7 -0
- metadata +386 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module OctocatalogDiff
|
|
4
|
+
class Catalog
|
|
5
|
+
# Represents a Puppet catalog that is read in directly from a JSON file.
|
|
6
|
+
class JSON
|
|
7
|
+
attr_accessor :node
|
|
8
|
+
attr_reader :error_message, :catalog, :catalog_json
|
|
9
|
+
|
|
10
|
+
# Constructor
|
|
11
|
+
# @param :json [String] REQUIRED: Content of catalog, will be parsed as JSON
|
|
12
|
+
# @param :node [String] Node name (if not supplied, will be determined from catalog)
|
|
13
|
+
def initialize(options)
|
|
14
|
+
raise ArgumentError, 'Usage: OctocatalogDiff::Catalog::JSON.initialize(options_hash)' unless options.is_a?(Hash)
|
|
15
|
+
raise ArgumentError, "Must supply :json as string in options: #{options[:json].class}" unless options[:json].is_a?(String)
|
|
16
|
+
@catalog_json = options[:json]
|
|
17
|
+
begin
|
|
18
|
+
@catalog = ::JSON.parse(@catalog_json)
|
|
19
|
+
@error_message = nil
|
|
20
|
+
@node ||= @catalog['name'] if @catalog.key?('name') # Puppet 4.x
|
|
21
|
+
@node ||= @catalog['data']['name'] if @catalog.key?('data') && @catalog['data'].is_a?(Hash) # Puppet 3.x
|
|
22
|
+
rescue ::JSON::ParserError => exc
|
|
23
|
+
@error_message = "Catalog JSON input failed to parse: #{exc.message}"
|
|
24
|
+
@catalog = nil
|
|
25
|
+
@catalog_json = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module OctocatalogDiff
|
|
4
|
+
class Catalog
|
|
5
|
+
# Represents a null Puppet catalog.
|
|
6
|
+
class Noop
|
|
7
|
+
attr_accessor :node
|
|
8
|
+
attr_reader :error_message, :catalog, :catalog_json
|
|
9
|
+
|
|
10
|
+
# Constructor
|
|
11
|
+
def initialize(options)
|
|
12
|
+
@catalog_json = '{"resources":[]}'
|
|
13
|
+
@catalog = { 'resources' => [] }
|
|
14
|
+
@error_message = nil
|
|
15
|
+
@node = options.fetch(:node, 'noop')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'stringio'
|
|
3
|
+
|
|
4
|
+
require_relative '../puppetdb'
|
|
5
|
+
|
|
6
|
+
module OctocatalogDiff
|
|
7
|
+
class Catalog
|
|
8
|
+
# Represents a Puppet catalog that is read from PuppetDB.
|
|
9
|
+
class PuppetDB
|
|
10
|
+
attr_accessor :node
|
|
11
|
+
attr_reader :error_message, :catalog, :catalog_json, :retries, :convert_file_resources
|
|
12
|
+
|
|
13
|
+
# Constructor - See OctocatalogDiff::PuppetDB for additional parameters
|
|
14
|
+
# @param :node [String] Node name
|
|
15
|
+
# @param :retry [Fixnum] Number of retries, if fetch fails
|
|
16
|
+
def initialize(options)
|
|
17
|
+
raise ArgumentError, 'Hash of options must be passed to OctocatalogDiff::Catalog::PuppetDB' unless options.is_a?(Hash)
|
|
18
|
+
raise ArgumentError, 'node must be a non-empty string' unless options[:node].is_a?(String) && options[:node] != ''
|
|
19
|
+
@node = options[:node]
|
|
20
|
+
@catalog = nil
|
|
21
|
+
@error_message = nil
|
|
22
|
+
@retries = nil
|
|
23
|
+
|
|
24
|
+
# Cannot convert file resources from this type of catalog
|
|
25
|
+
@convert_file_resources = false
|
|
26
|
+
|
|
27
|
+
# Save options
|
|
28
|
+
@options = options
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build(logger = Logger.new(StringIO.new))
|
|
32
|
+
fetch_catalog(logger)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Private method: Get catalog from PuppetDB. Sets @catalog / @catalog_json or @error_message
|
|
38
|
+
# @param logger [Logger object] Logger object
|
|
39
|
+
def fetch_catalog(logger)
|
|
40
|
+
# Use OctocatalogDiff::PuppetDB to interact with puppetdb
|
|
41
|
+
puppetdb_obj = OctocatalogDiff::PuppetDB.new(@options)
|
|
42
|
+
|
|
43
|
+
# Loop to retrieve catalog from PuppetDB
|
|
44
|
+
uri = "/pdb/query/v4/catalogs/#{@node}"
|
|
45
|
+
retries = @options.fetch(:retry, 1)
|
|
46
|
+
(retries + 1).times do
|
|
47
|
+
@retries = -1 if @retries.nil?
|
|
48
|
+
@retries += 1
|
|
49
|
+
begin
|
|
50
|
+
# Fetch catalog from PuppetDB
|
|
51
|
+
logger.debug "Retrieving #{@node} from #{uri}"
|
|
52
|
+
time_start = Time.now
|
|
53
|
+
result = puppetdb_obj.get(uri)
|
|
54
|
+
time_it_took = Time.now - time_start
|
|
55
|
+
|
|
56
|
+
# Validate received catalog
|
|
57
|
+
raise "PuppetDB catalog for #{@node} failed: no 'resources' hash in object" unless result.key?('resources')
|
|
58
|
+
raise "PuppetDB catalog for #{@node} failed: 'resources' was not a hash" unless result['resources'].is_a?(Hash)
|
|
59
|
+
logger.debug "Catalog for #{@node} retrieved from PuppetDB in #{time_it_took} seconds"
|
|
60
|
+
|
|
61
|
+
# Make this look like a generated catalog in Puppet 4.x
|
|
62
|
+
@catalog = result.merge('resources' => result['resources']['data'])
|
|
63
|
+
@catalog['resources'] = @catalog['resources'].map { |x| x.reject { |k, _v| k == 'resource' } }
|
|
64
|
+
|
|
65
|
+
# Set the other variables
|
|
66
|
+
@catalog_json = ::JSON.generate(@catalog)
|
|
67
|
+
@error_message = nil
|
|
68
|
+
rescue OctocatalogDiff::PuppetDB::ConnectionError => exc
|
|
69
|
+
@error_message = "Catalog retrieval failed (#{exc.class}) (#{exc.message})"
|
|
70
|
+
rescue OctocatalogDiff::PuppetDB::NotFoundError => exc
|
|
71
|
+
@error_message = "Node #{node} not found in PuppetDB (#{exc.message})"
|
|
72
|
+
rescue OctocatalogDiff::PuppetDB::PuppetDBError => exc
|
|
73
|
+
@error_message = "Catalog retrieval failed for node #{node} from PuppetDB (#{exc.message})"
|
|
74
|
+
rescue ::JSON::GeneratorError => exc
|
|
75
|
+
@error_message = "Failed to generate result from PuppetDB as JSON (#{exc.message})"
|
|
76
|
+
end
|
|
77
|
+
break if @catalog
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require_relative '../catalog-util/facts'
|
|
2
|
+
require_relative '../external/pson/pure'
|
|
3
|
+
require_relative '../util/httparty'
|
|
4
|
+
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
|
|
9
|
+
module OctocatalogDiff
|
|
10
|
+
class Catalog
|
|
11
|
+
# Represents a Puppet catalog that is obtained by contacting the Puppet Master.
|
|
12
|
+
class PuppetMaster
|
|
13
|
+
attr_accessor :node
|
|
14
|
+
attr_reader :error_message, :catalog, :catalog_json, :convert_file_resources, :options
|
|
15
|
+
|
|
16
|
+
# Defaults
|
|
17
|
+
DEFAULT_PUPPET_PORT_NUMBER = 8140
|
|
18
|
+
DEFAULT_PUPPET_SERVER_API = 3
|
|
19
|
+
PUPPET_MASTER_TIMEOUT = 60
|
|
20
|
+
|
|
21
|
+
# Constructor
|
|
22
|
+
# @param :node [String] Node name
|
|
23
|
+
# @param :retry [Fixnum] Number of retries, if fetch fails
|
|
24
|
+
# @param :branch [String] Environment to fetch from Puppet Master
|
|
25
|
+
# @param :puppet_master [String] Puppet server and port number (assumed to be DEFAULT_PUPPET_PORT_NUMBER if not given)
|
|
26
|
+
# @param :puppet_master_api_version [Fixnum] Puppet server API (default DEFAULT_PUPPET_SERVER_API)
|
|
27
|
+
# @param :puppet_master_ssl_ca [String] Path to file used to sign puppet master's certificate
|
|
28
|
+
# @param :puppet_master_ssl_verify [Boolean] Override the CA verification setting guessed from parameters
|
|
29
|
+
# @param :puppet_master_ssl_client_pem [String] PEM-encoded client key and certificate
|
|
30
|
+
# @param :puppet_master_ssl_client_p12 [String] pkcs12-encoded client key and certificate
|
|
31
|
+
# @param :puppet_master_ssl_client_password [String] Path to file containing password for SSL client key (any format)
|
|
32
|
+
# @param :puppet_master_ssl_client_auth [Boolean] Override the client-auth that is guessed from parameters
|
|
33
|
+
# @param :timeout [Fixnum] Connection timeout for Puppet master (default=PUPPET_MASTER_TIMEOUT seconds)
|
|
34
|
+
def initialize(options)
|
|
35
|
+
raise ArgumentError, 'Hash of options must be passed to OctocatalogDiff::Catalog::PuppetMaster' unless options.is_a?(Hash)
|
|
36
|
+
raise ArgumentError, 'node must be a non-empty string' unless options[:node].is_a?(String) && options[:node] != ''
|
|
37
|
+
unless options[:branch].is_a?(String) && options[:branch] != ''
|
|
38
|
+
raise ArgumentError, 'Environment must be a non-empty string'
|
|
39
|
+
end
|
|
40
|
+
unless options[:puppet_master].is_a?(String) && options[:puppet_master] != ''
|
|
41
|
+
raise ArgumentError, 'Puppet Master must be a non-empty string'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@node = options[:node]
|
|
45
|
+
@catalog = nil
|
|
46
|
+
@error_message = nil
|
|
47
|
+
@retries = nil
|
|
48
|
+
@timeout = options.fetch(:timeout, PUPPET_MASTER_TIMEOUT)
|
|
49
|
+
|
|
50
|
+
# Cannot convert file resources from this type of catalog right now.
|
|
51
|
+
# FIXME: This is possible with additional API calls but is current unimplemented.
|
|
52
|
+
@convert_file_resources = false
|
|
53
|
+
|
|
54
|
+
options[:puppet_master] += ":#{DEFAULT_PUPPET_PORT_NUMBER}" unless options[:puppet_master] =~ /\:\d+$/
|
|
55
|
+
@options = options
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build method
|
|
59
|
+
def build(logger = Logger.new(StringIO.new))
|
|
60
|
+
facts_obj = OctocatalogDiff::CatalogUtil::Facts.new(@options, logger)
|
|
61
|
+
logger.debug "Start retrieving facts for #{@node} from #{self.class}"
|
|
62
|
+
@facts = facts_obj.facts
|
|
63
|
+
logger.debug "Success retrieving facts for #{@node} from #{self.class}"
|
|
64
|
+
fetch_catalog(logger)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Returns a hash of parameters for each supported version of the Puppet Server Catalog API.
|
|
70
|
+
# @return [Hash] Hash of parameters
|
|
71
|
+
def puppet_catalog_api
|
|
72
|
+
{
|
|
73
|
+
2 => {
|
|
74
|
+
url: "https://#{@options[:puppet_master]}/#{@options[:branch]}/catalog/#{@node}",
|
|
75
|
+
parameters: {
|
|
76
|
+
'facts_format' => 'pson',
|
|
77
|
+
'facts' => @facts.fudge_timestamp.without('trusted').to_pson,
|
|
78
|
+
'transaction_uuid' => SecureRandom.uuid
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
3 => {
|
|
82
|
+
url: "https://#{@options[:puppet_master]}/puppet/v3/catalog/#{@node}",
|
|
83
|
+
parameters: {
|
|
84
|
+
'environment' => @options[:branch],
|
|
85
|
+
'facts_format' => 'pson',
|
|
86
|
+
'facts' => @facts.fudge_timestamp.without('trusted').to_pson,
|
|
87
|
+
'transaction_uuid' => SecureRandom.uuid
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Fetch catalog by contacting the Puppet master, sending the facts, and asking for the catalog. When the
|
|
94
|
+
# catalog is returned in PSON format, parse it to JSON and then set appropriate variables.
|
|
95
|
+
def fetch_catalog(logger)
|
|
96
|
+
api_version = @options[:puppet_master_api_version] || DEFAULT_PUPPET_SERVER_API
|
|
97
|
+
api = puppet_catalog_api[api_version]
|
|
98
|
+
raise ArgumentError, "Unsupported or invalid API version #{api_version}" unless api.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
logger.debug "Retrieve catalog from #{api[:url]} environment #{@options[:branch]}"
|
|
101
|
+
|
|
102
|
+
more_options = { headers: { 'Accept' => 'text/pson' }, timeout: @timeout }
|
|
103
|
+
post_hash = api[:parameters]
|
|
104
|
+
response = OctocatalogDiff::Util::HTTParty.post(api[:url], @options.merge(more_options), post_hash, 'puppet_master')
|
|
105
|
+
|
|
106
|
+
logger.debug "Response from #{api[:url]} environment #{@options[:branch]} was #{response[:code]}"
|
|
107
|
+
|
|
108
|
+
unless response[:code] == 200
|
|
109
|
+
@error_message = "Failed to retrieve catalog from #{api[:url]}: #{response[:code]} #{response[:body]}"
|
|
110
|
+
@catalog = nil
|
|
111
|
+
@catalog_json = nil
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@catalog = response[:parsed]
|
|
116
|
+
@catalog_json = ::JSON.generate(@catalog)
|
|
117
|
+
@error_message = nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Puppet - Automating Configuration Management.
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2005-2016 Puppet, Inc.
|
|
4
|
+
|
|
5
|
+
Puppet, Inc. can be contacted at: info@puppet.com
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# PSON
|
|
2
|
+
|
|
3
|
+
Puppet uses a JSON variant called PSON to serialize data (e.g. facts) transmitting to/from a Puppet master.
|
|
4
|
+
|
|
5
|
+
Documentation for PSON can be found here:
|
|
6
|
+
|
|
7
|
+
https://docs.puppet.com/puppet/4.6/reference/http_api/pson.html
|
|
8
|
+
|
|
9
|
+
The code in this directory was taken directly from Puppet and can be found at:
|
|
10
|
+
|
|
11
|
+
https://github.com/puppetlabs/puppet/tree/master/lib/puppet/external/pson
|
|
12
|
+
|
|
13
|
+
If you have found this code to deal with Puppet serialization, you should probably take the original and most up-to-date code from Puppet at the location above.
|
|
14
|
+
|
|
15
|
+
This code contains the following modifications:
|
|
16
|
+
|
|
17
|
+
- Change the `require` statements to `require_relative` statements so they work in this gem's directory structure
|
|
18
|
+
- Change `$MATCH` to `$&` because `$MATCH` is undefined without `require 'english`` or equivalent
|
|
19
|
+
|
|
20
|
+
This code is licensed by Puppet under the Apache 2.0 license. A copy of the Puppet license can be found in this directory.
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
require_relative 'version'
|
|
2
|
+
|
|
3
|
+
module PSON
|
|
4
|
+
class << self
|
|
5
|
+
# If _object_ is string-like parse the string and return the parsed result
|
|
6
|
+
# as a Ruby data structure. Otherwise generate a PSON text from the Ruby
|
|
7
|
+
# data structure object and return it.
|
|
8
|
+
#
|
|
9
|
+
# The _opts_ argument is passed through to generate/parse respectively, see
|
|
10
|
+
# generate and parse for their documentation.
|
|
11
|
+
def [](object, opts = {})
|
|
12
|
+
if object.respond_to? :to_str
|
|
13
|
+
PSON.parse(object.to_str, opts => {})
|
|
14
|
+
else
|
|
15
|
+
PSON.generate(object, opts => {})
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the PSON parser class, that is used by PSON. This might be either
|
|
20
|
+
# PSON::Ext::Parser or PSON::Pure::Parser.
|
|
21
|
+
attr_reader :parser
|
|
22
|
+
|
|
23
|
+
# Set the PSON parser class _parser_ to be used by PSON.
|
|
24
|
+
def parser=(parser) # :nodoc:
|
|
25
|
+
@parser = parser
|
|
26
|
+
remove_const :Parser if const_defined? :Parser
|
|
27
|
+
const_set :Parser, parser
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Return the constant located at _path_.
|
|
31
|
+
# Anything may be registered as a path by calling register_path, above.
|
|
32
|
+
# Otherwise, the format of _path_ has to be either ::A::B::C or A::B::C.
|
|
33
|
+
# In either of these cases A has to be defined in Object (e.g. the path
|
|
34
|
+
# must be an absolute namespace path. If the constant doesn't exist at
|
|
35
|
+
# the given path, an ArgumentError is raised.
|
|
36
|
+
def deep_const_get(path) # :nodoc:
|
|
37
|
+
path = path.to_s
|
|
38
|
+
path.split(/::/).inject(Object) do |p, c|
|
|
39
|
+
case
|
|
40
|
+
when c.empty? then p
|
|
41
|
+
when p.const_defined?(c) then p.const_get(c)
|
|
42
|
+
else raise ArgumentError, "can't find const for unregistered document type #{path}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Set the module _generator_ to be used by PSON.
|
|
48
|
+
def generator=(generator) # :nodoc:
|
|
49
|
+
@generator = generator
|
|
50
|
+
generator_methods = generator::GeneratorMethods
|
|
51
|
+
for const in generator_methods.constants
|
|
52
|
+
klass = deep_const_get(const)
|
|
53
|
+
modul = generator_methods.const_get(const)
|
|
54
|
+
klass.class_eval do
|
|
55
|
+
instance_methods(false).each do |m|
|
|
56
|
+
m.to_s == 'to_pson' and remove_method m
|
|
57
|
+
end
|
|
58
|
+
include modul
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
self.state = generator::State
|
|
62
|
+
const_set :State, self.state
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the PSON generator modul, that is used by PSON. This might be
|
|
66
|
+
# either PSON::Ext::Generator or PSON::Pure::Generator.
|
|
67
|
+
attr_reader :generator
|
|
68
|
+
|
|
69
|
+
# Returns the PSON generator state class, that is used by PSON. This might
|
|
70
|
+
# be either PSON::Ext::Generator::State or PSON::Pure::Generator::State.
|
|
71
|
+
attr_accessor :state
|
|
72
|
+
|
|
73
|
+
# This is create identifier, that is used to decide, if the _pson_create_
|
|
74
|
+
# hook of a class should be called. It defaults to 'document_type'.
|
|
75
|
+
attr_accessor :create_id
|
|
76
|
+
end
|
|
77
|
+
self.create_id = 'document_type'
|
|
78
|
+
|
|
79
|
+
NaN = (-1.0) ** 0.5
|
|
80
|
+
|
|
81
|
+
Infinity = 1.0/0
|
|
82
|
+
|
|
83
|
+
MinusInfinity = -Infinity
|
|
84
|
+
|
|
85
|
+
# The base exception for PSON errors.
|
|
86
|
+
class PSONError < StandardError; end
|
|
87
|
+
|
|
88
|
+
# This exception is raised, if a parser error occurs.
|
|
89
|
+
class ParserError < PSONError; end
|
|
90
|
+
|
|
91
|
+
# This exception is raised, if the nesting of parsed datastructures is too
|
|
92
|
+
# deep.
|
|
93
|
+
class NestingError < ParserError; end
|
|
94
|
+
|
|
95
|
+
# This exception is raised, if a generator or unparser error occurs.
|
|
96
|
+
class GeneratorError < PSONError; end
|
|
97
|
+
# For backwards compatibility
|
|
98
|
+
UnparserError = GeneratorError
|
|
99
|
+
|
|
100
|
+
# If a circular data structure is encountered while unparsing
|
|
101
|
+
# this exception is raised.
|
|
102
|
+
class CircularDatastructure < GeneratorError; end
|
|
103
|
+
|
|
104
|
+
# This exception is raised, if the required unicode support is missing on the
|
|
105
|
+
# system. Usually this means, that the iconv library is not installed.
|
|
106
|
+
class MissingUnicodeSupport < PSONError; end
|
|
107
|
+
|
|
108
|
+
module_function
|
|
109
|
+
|
|
110
|
+
# Parse the PSON string _source_ into a Ruby data structure and return it.
|
|
111
|
+
#
|
|
112
|
+
# _opts_ can have the following
|
|
113
|
+
# keys:
|
|
114
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
|
|
115
|
+
# structures. Disable depth checking with :max_nesting => false, it defaults
|
|
116
|
+
# to 19.
|
|
117
|
+
# * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
|
|
118
|
+
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
|
|
119
|
+
# to false.
|
|
120
|
+
def parse(source, opts = {})
|
|
121
|
+
PSON.parser.new(source, opts).parse
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Parse the PSON string _source_ into a Ruby data structure and return it.
|
|
125
|
+
# The bang version of the parse method, defaults to the more dangerous values
|
|
126
|
+
# for the _opts_ hash, so be sure only to parse trusted _source_ strings.
|
|
127
|
+
#
|
|
128
|
+
# _opts_ can have the following keys:
|
|
129
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
|
|
130
|
+
# structures. Enable depth checking with :max_nesting => anInteger. The parse!
|
|
131
|
+
# methods defaults to not doing max depth checking: This can be dangerous,
|
|
132
|
+
# if someone wants to fill up your stack.
|
|
133
|
+
# * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
|
|
134
|
+
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
|
|
135
|
+
# to true.
|
|
136
|
+
def parse!(source, opts = {})
|
|
137
|
+
opts = {
|
|
138
|
+
:max_nesting => false,
|
|
139
|
+
:allow_nan => true
|
|
140
|
+
}.update(opts)
|
|
141
|
+
PSON.parser.new(source, opts).parse
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Unparse the Ruby data structure _obj_ into a single line PSON string and
|
|
145
|
+
# return it. _state_ is
|
|
146
|
+
# * a PSON::State object,
|
|
147
|
+
# * or a Hash like object (responding to to_hash),
|
|
148
|
+
# * an object convertible into a hash by a to_h method,
|
|
149
|
+
# that is used as or to configure a State object.
|
|
150
|
+
#
|
|
151
|
+
# It defaults to a state object, that creates the shortest possible PSON text
|
|
152
|
+
# in one line, checks for circular data structures and doesn't allow NaN,
|
|
153
|
+
# Infinity, and -Infinity.
|
|
154
|
+
#
|
|
155
|
+
# A _state_ hash can have the following keys:
|
|
156
|
+
# * *indent*: a string used to indent levels (default: ''),
|
|
157
|
+
# * *space*: a string that is put after, a : or , delimiter (default: ''),
|
|
158
|
+
# * *space_before*: a string that is put before a : pair delimiter (default: ''),
|
|
159
|
+
# * *object_nl*: a string that is put at the end of a PSON object (default: ''),
|
|
160
|
+
# * *array_nl*: a string that is put at the end of a PSON array (default: ''),
|
|
161
|
+
# * *check_circular*: true if checking for circular data structures
|
|
162
|
+
# should be done (the default), false otherwise.
|
|
163
|
+
# * *allow_nan*: true if NaN, Infinity, and -Infinity should be
|
|
164
|
+
# generated, otherwise an exception is thrown, if these values are
|
|
165
|
+
# encountered. This options defaults to false.
|
|
166
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the data
|
|
167
|
+
# structures from which PSON is to be generated. Disable depth checking
|
|
168
|
+
# with :max_nesting => false, it defaults to 19.
|
|
169
|
+
#
|
|
170
|
+
# See also the fast_generate for the fastest creation method with the least
|
|
171
|
+
# amount of sanity checks, and the pretty_generate method for some
|
|
172
|
+
# defaults for a pretty output.
|
|
173
|
+
def generate(obj, state = nil)
|
|
174
|
+
if state
|
|
175
|
+
state = State.from_state(state)
|
|
176
|
+
else
|
|
177
|
+
state = State.new
|
|
178
|
+
end
|
|
179
|
+
obj.to_pson(state)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# :stopdoc:
|
|
183
|
+
# I want to deprecate these later, so I'll first be silent about them, and
|
|
184
|
+
# later delete them.
|
|
185
|
+
alias unparse generate
|
|
186
|
+
module_function :unparse
|
|
187
|
+
# :startdoc:
|
|
188
|
+
|
|
189
|
+
# Unparse the Ruby data structure _obj_ into a single line PSON string and
|
|
190
|
+
# return it. This method disables the checks for circles in Ruby objects, and
|
|
191
|
+
# also generates NaN, Infinity, and, -Infinity float values.
|
|
192
|
+
#
|
|
193
|
+
# *WARNING*: Be careful not to pass any Ruby data structures with circles as
|
|
194
|
+
# _obj_ argument, because this will cause PSON to go into an infinite loop.
|
|
195
|
+
def fast_generate(obj)
|
|
196
|
+
obj.to_pson(nil)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# :stopdoc:
|
|
200
|
+
# I want to deprecate these later, so I'll first be silent about them, and later delete them.
|
|
201
|
+
alias fast_unparse fast_generate
|
|
202
|
+
module_function :fast_unparse
|
|
203
|
+
# :startdoc:
|
|
204
|
+
|
|
205
|
+
# Unparse the Ruby data structure _obj_ into a PSON string and return it. The
|
|
206
|
+
# returned string is a prettier form of the string returned by #unparse.
|
|
207
|
+
#
|
|
208
|
+
# The _opts_ argument can be used to configure the generator, see the
|
|
209
|
+
# generate method for a more detailed explanation.
|
|
210
|
+
def pretty_generate(obj, opts = nil)
|
|
211
|
+
|
|
212
|
+
state = PSON.state.new(
|
|
213
|
+
|
|
214
|
+
:indent => ' ',
|
|
215
|
+
:space => ' ',
|
|
216
|
+
:object_nl => "\n",
|
|
217
|
+
:array_nl => "\n",
|
|
218
|
+
|
|
219
|
+
:check_circular => true
|
|
220
|
+
)
|
|
221
|
+
if opts
|
|
222
|
+
if opts.respond_to? :to_hash
|
|
223
|
+
opts = opts.to_hash
|
|
224
|
+
elsif opts.respond_to? :to_h
|
|
225
|
+
opts = opts.to_h
|
|
226
|
+
else
|
|
227
|
+
raise TypeError, "can't convert #{opts.class} into Hash"
|
|
228
|
+
end
|
|
229
|
+
state.configure(opts)
|
|
230
|
+
end
|
|
231
|
+
obj.to_pson(state)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# :stopdoc:
|
|
235
|
+
# I want to deprecate these later, so I'll first be silent about them, and later delete them.
|
|
236
|
+
alias pretty_unparse pretty_generate
|
|
237
|
+
module_function :pretty_unparse
|
|
238
|
+
# :startdoc:
|
|
239
|
+
|
|
240
|
+
# Load a ruby data structure from a PSON _source_ and return it. A source can
|
|
241
|
+
# either be a string-like object, an IO like object, or an object responding
|
|
242
|
+
# to the read method. If _proc_ was given, it will be called with any nested
|
|
243
|
+
# Ruby object as an argument recursively in depth first order.
|
|
244
|
+
#
|
|
245
|
+
# This method is part of the implementation of the load/dump interface of
|
|
246
|
+
# Marshal and YAML.
|
|
247
|
+
def load(source, proc = nil)
|
|
248
|
+
if source.respond_to? :to_str
|
|
249
|
+
source = source.to_str
|
|
250
|
+
elsif source.respond_to? :to_io
|
|
251
|
+
source = source.to_io.read
|
|
252
|
+
else
|
|
253
|
+
source = source.read
|
|
254
|
+
end
|
|
255
|
+
result = parse(source, :max_nesting => false, :allow_nan => true)
|
|
256
|
+
recurse_proc(result, &proc) if proc
|
|
257
|
+
result
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def recurse_proc(result, &proc)
|
|
261
|
+
case result
|
|
262
|
+
when Array
|
|
263
|
+
result.each { |x| recurse_proc x, &proc }
|
|
264
|
+
proc.call result
|
|
265
|
+
when Hash
|
|
266
|
+
result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
|
|
267
|
+
proc.call result
|
|
268
|
+
else
|
|
269
|
+
proc.call result
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
private :recurse_proc
|
|
273
|
+
module_function :recurse_proc
|
|
274
|
+
|
|
275
|
+
alias restore load
|
|
276
|
+
module_function :restore
|
|
277
|
+
|
|
278
|
+
# Dumps _obj_ as a PSON string, i.e. calls generate on the object and returns
|
|
279
|
+
# the result.
|
|
280
|
+
#
|
|
281
|
+
# If anIO (an IO like object or an object that responds to the write method)
|
|
282
|
+
# was given, the resulting PSON is written to it.
|
|
283
|
+
#
|
|
284
|
+
# If the number of nested arrays or objects exceeds _limit_ an ArgumentError
|
|
285
|
+
# exception is raised. This argument is similar (but not exactly the
|
|
286
|
+
# same!) to the _limit_ argument in Marshal.dump.
|
|
287
|
+
#
|
|
288
|
+
# This method is part of the implementation of the load/dump interface of
|
|
289
|
+
# Marshal and YAML.
|
|
290
|
+
def dump(obj, anIO = nil, limit = nil)
|
|
291
|
+
if anIO and limit.nil?
|
|
292
|
+
anIO = anIO.to_io if anIO.respond_to?(:to_io)
|
|
293
|
+
unless anIO.respond_to?(:write)
|
|
294
|
+
limit = anIO
|
|
295
|
+
anIO = nil
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
limit ||= 0
|
|
299
|
+
result = generate(obj, :allow_nan => true, :max_nesting => limit)
|
|
300
|
+
if anIO
|
|
301
|
+
anIO.write result
|
|
302
|
+
anIO
|
|
303
|
+
else
|
|
304
|
+
result
|
|
305
|
+
end
|
|
306
|
+
rescue PSON::NestingError
|
|
307
|
+
raise ArgumentError, "exceed depth limit", $!.backtrace
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# Provide a smarter wrapper for changing string encoding that works with
|
|
312
|
+
# both Ruby 1.8 (iconv) and 1.9 (String#encode). Thankfully they seem to
|
|
313
|
+
# have compatible input syntax, at least for the encodings we touch.
|
|
314
|
+
if String.method_defined?("encode")
|
|
315
|
+
def encode(to, from, string)
|
|
316
|
+
string.encode(to, from)
|
|
317
|
+
end
|
|
318
|
+
else
|
|
319
|
+
require 'iconv'
|
|
320
|
+
def encode(to, from, string)
|
|
321
|
+
Iconv.conv(to, from, string)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
module ::Kernel
|
|
327
|
+
private
|
|
328
|
+
|
|
329
|
+
# Outputs _objs_ to STDOUT as PSON strings in the shortest form, that is in
|
|
330
|
+
# one line.
|
|
331
|
+
def j(*objs)
|
|
332
|
+
objs.each do |obj|
|
|
333
|
+
puts PSON::generate(obj, :allow_nan => true, :max_nesting => false)
|
|
334
|
+
end
|
|
335
|
+
nil
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Outputs _objs_ to STDOUT as PSON strings in a pretty format, with
|
|
339
|
+
# indentation and over many lines.
|
|
340
|
+
def jj(*objs)
|
|
341
|
+
objs.each do |obj|
|
|
342
|
+
puts PSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
|
|
343
|
+
end
|
|
344
|
+
nil
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# If _object_ is string-like parse the string and return the parsed result as
|
|
348
|
+
# a Ruby data structure. Otherwise generate a PSON text from the Ruby data
|
|
349
|
+
# structure object and return it.
|
|
350
|
+
#
|
|
351
|
+
# The _opts_ argument is passed through to generate/parse respectively, see
|
|
352
|
+
# generate and parse for their documentation.
|
|
353
|
+
def PSON(object, opts = {})
|
|
354
|
+
if object.respond_to? :to_str
|
|
355
|
+
PSON.parse(object.to_str, opts)
|
|
356
|
+
else
|
|
357
|
+
PSON.generate(object, opts)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
class ::Class
|
|
363
|
+
# Returns true, if this class can be used to create an instance
|
|
364
|
+
# from a serialised PSON string. The class has to implement a class
|
|
365
|
+
# method _pson_create_ that expects a hash as first parameter, which includes
|
|
366
|
+
# the required data.
|
|
367
|
+
def pson_creatable?
|
|
368
|
+
respond_to?(:pson_create)
|
|
369
|
+
end
|
|
370
|
+
end
|