etch 3.19.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +1 -1
- data/bin/etch +10 -4
- data/bin/etch_cron_wrapper +1 -1
- data/bin/etch_to_trunk +1 -1
- data/lib/etch.rb +51 -39
- data/lib/{etchclient.rb → etch/client.rb} +68 -47
- data/lib/silently.rb +11 -0
- metadata +34 -40
data/Rakefile
CHANGED
@@ -3,7 +3,7 @@ spec = Gem::Specification.new do |s|
|
|
3
3
|
s.name = 'etch'
|
4
4
|
s.summary = 'Etch system configuration management client'
|
5
5
|
s.add_dependency('facter')
|
6
|
-
s.version = '3.
|
6
|
+
s.version = '3.20.0'
|
7
7
|
s.author = 'Jason Heiss'
|
8
8
|
s.email = 'etch-users@lists.sourceforge.net'
|
9
9
|
s.homepage = 'http://etch.sourceforge.net'
|
data/bin/etch
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
#!/usr/bin/ruby
|
1
|
+
#!/usr/bin/ruby
|
2
2
|
##############################################################################
|
3
3
|
# Etch configuration file management tool
|
4
4
|
##############################################################################
|
5
5
|
|
6
|
-
# Ensure we can find
|
7
|
-
$:.unshift(File.dirname(__FILE__))
|
6
|
+
# Ensure we can find etch/client.rb when run within the development repository
|
7
|
+
$:.unshift(File.join(File.dirname(File.dirname(__FILE__)), 'lib'))
|
8
8
|
|
9
9
|
require 'optparse'
|
10
|
-
require '
|
10
|
+
require 'etch/client'
|
11
11
|
|
12
12
|
#
|
13
13
|
# Parse the command line options
|
@@ -69,6 +69,12 @@ end
|
|
69
69
|
opts.on('--key PRIVATE_KEY', 'Use this private key for signing messages to server.') do |opt|
|
70
70
|
options[:key] = opt
|
71
71
|
end
|
72
|
+
opts.on('--configdir DIR', 'Directory containing etch.conf, defaults to /etc') do |opt|
|
73
|
+
options[:configdir] = opt
|
74
|
+
end
|
75
|
+
opts.on('--vardir DIR', 'Directory for etch state, defaults to /var/etch') do |opt|
|
76
|
+
options[:vardir] = opt
|
77
|
+
end
|
72
78
|
opts.on('--test-root TESTDIR', 'For use by the test suite only.') do |opt|
|
73
79
|
options[:file_system_root] = opt
|
74
80
|
end
|
data/bin/etch_cron_wrapper
CHANGED
data/bin/etch_to_trunk
CHANGED
data/lib/etch.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
require '
|
5
|
-
|
6
|
-
require '
|
1
|
+
# Exclude standard libraries and gems from the warnings induced by
|
2
|
+
# running ruby with the -w flag. Several of these have warnings under
|
3
|
+
# ruby 1.9 and there's nothing we can do to fix that.
|
4
|
+
require 'silently'
|
5
|
+
Silently.silently do
|
6
|
+
require 'find' # Find.find
|
7
|
+
require 'pathname' # absolute?
|
8
|
+
require 'digest/sha1' # hexdigest
|
9
|
+
require 'base64' # decode64, encode64
|
10
|
+
require 'fileutils' # mkdir_p
|
11
|
+
require 'erb'
|
12
|
+
require 'logger'
|
13
|
+
end
|
7
14
|
require 'versiontype' # Version
|
8
|
-
require 'logger'
|
9
15
|
|
10
16
|
class Etch
|
11
17
|
def self.xmllib
|
@@ -16,27 +22,29 @@ class Etch
|
|
16
22
|
end
|
17
23
|
end
|
18
24
|
|
19
|
-
# By default we try to use
|
20
|
-
# available. The xmllib environment variable can be used to force
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
25
|
+
# By default we try to use nokogiri, falling back to rexml if it is not
|
26
|
+
# available. The xmllib environment variable can be used to force a specific
|
27
|
+
# library, mostly for testing purposes.
|
28
|
+
Silently.silently do
|
29
|
+
begin
|
30
|
+
if !ENV['xmllib'] || ENV['xmllib'] == 'nokogiri'
|
31
|
+
require 'rubygems' # nokogiri is a gem
|
32
|
+
require 'nokogiri'
|
33
|
+
Etch.xmllib = :nokogiri
|
34
|
+
elsif ENV['xmllib'] == 'libxml'
|
35
|
+
require 'rubygems' # libxml is a gem
|
36
|
+
require 'libxml'
|
37
|
+
Etch.xmllib = :libxml
|
38
|
+
else
|
39
|
+
raise LoadError
|
40
|
+
end
|
41
|
+
rescue LoadError
|
42
|
+
if !ENV['xmllib'] || ENV['xmllib'] == 'rexml'
|
43
|
+
require 'rexml/document'
|
44
|
+
Etch.xmllib = :rexml
|
45
|
+
else
|
46
|
+
raise
|
47
|
+
end
|
40
48
|
end
|
41
49
|
end
|
42
50
|
|
@@ -170,10 +178,12 @@ class Etch
|
|
170
178
|
filelist = []
|
171
179
|
if request.empty?
|
172
180
|
@dlogger.debug "Building complete file list for request from #{@fqdn}"
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
181
|
+
if File.exist?(@sourcebase)
|
182
|
+
Find.find(@sourcebase) do |path|
|
183
|
+
if File.directory?(path) && File.exist?(File.join(path, 'config.xml'))
|
184
|
+
# Strip @sourcebase from start of path
|
185
|
+
filelist << path.sub(Regexp.new('^' + Regexp.escape(@sourcebase)), '')
|
186
|
+
end
|
177
187
|
end
|
178
188
|
end
|
179
189
|
elsif request[:files]
|
@@ -206,9 +216,11 @@ class Etch
|
|
206
216
|
commandnames = []
|
207
217
|
if request.empty?
|
208
218
|
@dlogger.debug "Building complete configuration commands for request from #{@fqdn}"
|
209
|
-
|
210
|
-
|
211
|
-
|
219
|
+
if File.exist?(@commandsbase)
|
220
|
+
Find.find(@commandsbase) do |path|
|
221
|
+
if File.directory?(path) && File.exist?(File.join(path, 'commands.xml'))
|
222
|
+
commandnames << File.basename(path)
|
223
|
+
end
|
212
224
|
end
|
213
225
|
end
|
214
226
|
elsif request[:commands]
|
@@ -363,7 +375,7 @@ class Etch
|
|
363
375
|
|
364
376
|
# Change into the corresponding directory so that the user can
|
365
377
|
# refer to source files and scripts by their relative pathnames.
|
366
|
-
Dir
|
378
|
+
Dir.chdir "#{@sourcebase}/#{file}"
|
367
379
|
|
368
380
|
# See what type of action the user has requested
|
369
381
|
|
@@ -414,7 +426,7 @@ class Etch
|
|
414
426
|
|
415
427
|
# Just slurp the file in
|
416
428
|
plain_file = Etch.xmltext(plain_elements.first)
|
417
|
-
newcontents = IO
|
429
|
+
newcontents = IO.read(plain_file)
|
418
430
|
elsif Etch.xmlfindfirst(config_xml, '/config/file/source/template')
|
419
431
|
template_elements = Etch.xmlarray(config_xml, '/config/file/source/template')
|
420
432
|
if check_for_inconsistency(template_elements)
|
@@ -913,7 +925,7 @@ class Etch
|
|
913
925
|
|
914
926
|
# Change into the corresponding directory so that the user can
|
915
927
|
# refer to source files and scripts by their relative pathnames.
|
916
|
-
Dir
|
928
|
+
Dir.chdir "#{@commandsbase}/#{command}"
|
917
929
|
|
918
930
|
# Check that the resulting document is consistent after filtering
|
919
931
|
remove = []
|
@@ -1360,7 +1372,7 @@ class Etch
|
|
1360
1372
|
when :nokogiri
|
1361
1373
|
destelem << elem.dup
|
1362
1374
|
when :rexml
|
1363
|
-
destelem.add_element(elem.
|
1375
|
+
destelem.add_element(elem.deep_clone)
|
1364
1376
|
else
|
1365
1377
|
raise "Unknown XML library #{Etch.xmllib}"
|
1366
1378
|
end
|
@@ -3,46 +3,53 @@
|
|
3
3
|
##############################################################################
|
4
4
|
|
5
5
|
# Ensure we can find etch.rb if run within the development directory structure
|
6
|
-
# This is roughly equivalent to "
|
7
|
-
serverlibdir = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'server', 'lib')
|
6
|
+
# This is roughly equivalent to "../../../server/lib"
|
7
|
+
serverlibdir = File.join(File.dirname(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__))))), 'server', 'lib')
|
8
8
|
if File.exist?(serverlibdir)
|
9
9
|
$:.unshift(serverlibdir)
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
# Exclude standard libraries and gems from the warnings induced by
|
13
|
+
# running ruby with the -w flag. Several of these have warnings under
|
14
|
+
# ruby 1.9 and there's nothing we can do to fix that.
|
15
|
+
require 'silently'
|
16
|
+
Silently.silently do
|
17
|
+
begin
|
18
|
+
# Try loading facter w/o gems first so that we don't introduce a
|
19
|
+
# dependency on gems if it is not needed.
|
20
|
+
require 'facter' # Facter
|
21
|
+
rescue LoadError
|
22
|
+
require 'rubygems'
|
23
|
+
require 'facter'
|
24
|
+
end
|
25
|
+
require 'find'
|
26
|
+
require 'digest/sha1' # hexdigest
|
27
|
+
require 'openssl' # OpenSSL
|
28
|
+
require 'base64' # decode64, encode64
|
29
|
+
require 'uri'
|
30
|
+
require 'net/http'
|
31
|
+
require 'net/https'
|
32
|
+
require 'rexml/document'
|
33
|
+
require 'fileutils' # copy, mkpath, rmtree
|
34
|
+
require 'fcntl' # Fcntl::O_*
|
35
|
+
require 'etc' # getpwnam, getgrnam
|
36
|
+
require 'tempfile' # Tempfile
|
37
|
+
require 'find' # Find.find
|
38
|
+
require 'cgi'
|
39
|
+
require 'timeout'
|
40
|
+
require 'logger'
|
19
41
|
end
|
20
|
-
require 'find'
|
21
|
-
require 'digest/sha1' # hexdigest
|
22
|
-
require 'openssl' # OpenSSL
|
23
|
-
require 'base64' # decode64, encode64
|
24
|
-
require 'uri'
|
25
|
-
require 'net/http'
|
26
|
-
require 'net/https'
|
27
|
-
require 'rexml/document'
|
28
|
-
require 'fileutils' # copy, mkpath, rmtree
|
29
|
-
require 'fcntl' # Fcntl::O_*
|
30
|
-
require 'etc' # getpwnam, getgrnam
|
31
|
-
require 'tempfile' # Tempfile
|
32
|
-
require 'cgi'
|
33
|
-
require 'timeout'
|
34
|
-
require 'logger'
|
35
42
|
require 'etch'
|
36
43
|
|
37
44
|
class Etch::Client
|
38
|
-
VERSION = '3.
|
45
|
+
VERSION = '3.20.0'
|
39
46
|
|
40
47
|
CONFIRM_PROCEED = 1
|
41
48
|
CONFIRM_SKIP = 2
|
42
49
|
CONFIRM_QUIT = 3
|
43
50
|
PRIVATE_KEY_PATHS = ["/etc/ssh/ssh_host_rsa_key", "/etc/ssh_host_rsa_key"]
|
44
51
|
DEFAULT_CONFIGDIR = '/etc'
|
45
|
-
|
52
|
+
DEFAULT_VARDIR = '/var/etch'
|
46
53
|
DEFAULT_DETAILED_RESULTS = ['SERVER']
|
47
54
|
|
48
55
|
# We need these in relation to the output capturing
|
@@ -53,6 +60,8 @@ class Etch::Client
|
|
53
60
|
|
54
61
|
def initialize(options)
|
55
62
|
@server = options[:server] ? options[:server] : 'https://etch'
|
63
|
+
@configdir = options[:configdir] ? options[:configdir] : DEFAULT_CONFIGDIR
|
64
|
+
@vardir = options[:vardir] ? options[:vardir] : DEFAULT_VARDIR
|
56
65
|
@tag = options[:tag]
|
57
66
|
@local = options[:local] ? File.expand_path(options[:local]) : nil
|
58
67
|
@debug = options[:debug]
|
@@ -64,17 +73,14 @@ class Etch::Client
|
|
64
73
|
@key = options[:key] ? options[:key] : get_private_key_path
|
65
74
|
@disableforce = options[:disableforce]
|
66
75
|
@lockforce = options[:lockforce]
|
67
|
-
|
68
|
-
@last_response = ""
|
69
76
|
|
70
|
-
@
|
71
|
-
@varbase = DEFAULT_VARBASE
|
77
|
+
@last_response = ""
|
72
78
|
|
73
79
|
@file_system_root = '/' # Not sure if this needs to be more portable
|
74
80
|
# This option is only intended for use by the test suite
|
75
81
|
if options[:file_system_root]
|
76
82
|
@file_system_root = options[:file_system_root]
|
77
|
-
@
|
83
|
+
@vardir = File.join(@file_system_root, @vardir)
|
78
84
|
@configdir = File.join(@file_system_root, @configdir)
|
79
85
|
end
|
80
86
|
|
@@ -142,10 +148,10 @@ class Etch::Client
|
|
142
148
|
@detailed_results = DEFAULT_DETAILED_RESULTS
|
143
149
|
end
|
144
150
|
|
145
|
-
@origbase = File.join(@
|
146
|
-
@historybase = File.join(@
|
147
|
-
@lockbase = File.join(@
|
148
|
-
@requestbase = File.join(@
|
151
|
+
@origbase = File.join(@vardir, 'orig')
|
152
|
+
@historybase = File.join(@vardir, 'history')
|
153
|
+
@lockbase = File.join(@vardir, 'locks')
|
154
|
+
@requestbase = File.join(@vardir, 'requests')
|
149
155
|
|
150
156
|
@facts = Facter.to_hash
|
151
157
|
if @facts['operatingsystemrelease']
|
@@ -163,6 +169,10 @@ class Etch::Client
|
|
163
169
|
else
|
164
170
|
dlogger.level = Logger::INFO
|
165
171
|
end
|
172
|
+
blankrequest = {}
|
173
|
+
@facts.each_pair { |key, value| blankrequest[key] = value.to_s }
|
174
|
+
blankrequest['fqdn'] = @facts['fqdn']
|
175
|
+
@facts = blankrequest
|
166
176
|
@etch = Etch.new(logger, dlogger)
|
167
177
|
else
|
168
178
|
# Make sure the server URL ends in a / so that we can append paths
|
@@ -215,6 +225,7 @@ class Etch::Client
|
|
215
225
|
# Prep http instance
|
216
226
|
http = nil
|
217
227
|
if !@local
|
228
|
+
puts "Connecting to #{@filesuri}" if (@debug)
|
218
229
|
http = Net::HTTP.new(@filesuri.host, @filesuri.port)
|
219
230
|
if @filesuri.scheme == "https"
|
220
231
|
# Eliminate the OpenSSL "using default DH parameters" warning
|
@@ -565,7 +576,7 @@ class Etch::Client
|
|
565
576
|
end
|
566
577
|
|
567
578
|
def check_for_disable_etch_file
|
568
|
-
disable_etch = File.join(@
|
579
|
+
disable_etch = File.join(@vardir, 'disable_etch')
|
569
580
|
message = ''
|
570
581
|
if File.exist?(disable_etch)
|
571
582
|
if !@disableforce
|
@@ -1803,7 +1814,7 @@ class Etch::Client
|
|
1803
1814
|
puts "Original file #{file} doesn't exist, saving that state permanently as #{origpath}"
|
1804
1815
|
end
|
1805
1816
|
if proceed
|
1806
|
-
File.open(origpath, 'w') { |
|
1817
|
+
File.open(origpath, 'w') { |origfile| } if (!@dryrun)
|
1807
1818
|
end
|
1808
1819
|
end
|
1809
1820
|
|
@@ -2014,7 +2025,7 @@ class Etch::Client
|
|
2014
2025
|
else
|
2015
2026
|
# If there's no file to back up then leave a marker file so
|
2016
2027
|
# that restore_backup does the right thing
|
2017
|
-
File.open("#{backuppath}.NOORIG", "w") { |
|
2028
|
+
File.open("#{backuppath}.NOORIG", "w") { |markerfile| }
|
2018
2029
|
end
|
2019
2030
|
end
|
2020
2031
|
|
@@ -2375,7 +2386,7 @@ class Etch::Client
|
|
2375
2386
|
begin
|
2376
2387
|
fd = IO::sysopen(lockpath, Fcntl::O_WRONLY|Fcntl::O_CREAT|Fcntl::O_EXCL)
|
2377
2388
|
puts "Lock acquired for #{file}" if (@debug)
|
2378
|
-
f = IO.open(fd) { |
|
2389
|
+
f = IO.open(fd) { |lockfile| lockfile.puts $$ }
|
2379
2390
|
@locked_files[file] = true
|
2380
2391
|
return
|
2381
2392
|
rescue Errno::EEXIST
|
@@ -2420,13 +2431,15 @@ class Etch::Client
|
|
2420
2431
|
# and can be removed. If told to force we remove all lockfiles.
|
2421
2432
|
def remove_stale_lock_files
|
2422
2433
|
twohoursago = Time.at(Time.now - 60 * 60 * 2)
|
2423
|
-
|
2424
|
-
|
2425
|
-
|
2426
|
-
|
2427
|
-
|
2428
|
-
|
2429
|
-
|
2434
|
+
if File.exist?(@lockbase)
|
2435
|
+
Find.find(@lockbase) do |file|
|
2436
|
+
next unless file =~ /\.LOCK$/
|
2437
|
+
next unless File.file?(file)
|
2438
|
+
|
2439
|
+
if @lockforce || File.mtime(file) < twohoursago
|
2440
|
+
puts "Removing stale lock file #{file}"
|
2441
|
+
File.delete(file)
|
2442
|
+
end
|
2430
2443
|
end
|
2431
2444
|
end
|
2432
2445
|
end
|
@@ -2439,6 +2452,8 @@ class Etch::Client
|
|
2439
2452
|
# for etch to handle any given file, including running any
|
2440
2453
|
# setup/pre/post commands.
|
2441
2454
|
OUTPUT_CAPTURE_TIMEOUT = 5 * 60
|
2455
|
+
# In interactive mode bump the timeout up to something absurdly large
|
2456
|
+
OUTPUT_CAPTURE_INTERACTIVE_TIMEOUT = 14 * 24 * 60 * 60
|
2442
2457
|
def start_output_capture
|
2443
2458
|
# Establish a pipe, spawn a child process, and redirect stdout/stderr
|
2444
2459
|
# to the pipe. The child gathers up anything sent over the pipe and
|
@@ -2490,7 +2505,13 @@ class Etch::Client
|
|
2490
2505
|
# capturing feature this results in etch hanging around forever
|
2491
2506
|
# waiting for the pipes to close. We time out after a suitable
|
2492
2507
|
# period of time so that etch processes don't hang around forever.
|
2493
|
-
|
2508
|
+
timeout = nil
|
2509
|
+
if @interactive
|
2510
|
+
timeout = OUTPUT_CAPTURE_INTERACTIVE_TIMEOUT
|
2511
|
+
else
|
2512
|
+
timeout = OUTPUT_CAPTURE_TIMEOUT
|
2513
|
+
end
|
2514
|
+
Timeout.timeout(timeout) do
|
2494
2515
|
while char = pread.getc
|
2495
2516
|
putc(char)
|
2496
2517
|
output << char.chr
|
data/lib/silently.rb
ADDED
metadata
CHANGED
@@ -1,72 +1,66 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: etch
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.20.0
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
|
-
authors:
|
7
|
+
authors:
|
7
8
|
- Jason Heiss
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
dependencies:
|
15
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2012-03-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
16
15
|
name: facter
|
16
|
+
requirement: &70362060167060 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
17
22
|
type: :runtime
|
18
|
-
|
19
|
-
version_requirements:
|
20
|
-
requirements:
|
21
|
-
- - ">="
|
22
|
-
- !ruby/object:Gem::Version
|
23
|
-
version: "0"
|
24
|
-
version:
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70362060167060
|
25
25
|
description:
|
26
26
|
email: etch-users@lists.sourceforge.net
|
27
|
-
executables:
|
27
|
+
executables:
|
28
28
|
- etch
|
29
29
|
- etch_to_trunk
|
30
30
|
- etch_cron_wrapper
|
31
31
|
extensions: []
|
32
|
-
|
33
32
|
extra_rdoc_files: []
|
34
|
-
|
35
|
-
files:
|
33
|
+
files:
|
36
34
|
- bin/etch
|
37
35
|
- bin/etch_cron_wrapper
|
38
36
|
- bin/etch_to_trunk
|
37
|
+
- lib/etch/client.rb
|
39
38
|
- lib/etch.rb
|
40
|
-
- lib/
|
39
|
+
- lib/silently.rb
|
41
40
|
- lib/versiontype.rb
|
42
41
|
- Rakefile
|
43
|
-
has_rdoc: true
|
44
42
|
homepage: http://etch.sourceforge.net
|
45
43
|
licenses: []
|
46
|
-
|
47
44
|
post_install_message:
|
48
45
|
rdoc_options: []
|
49
|
-
|
50
|
-
require_paths:
|
46
|
+
require_paths:
|
51
47
|
- lib
|
52
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.8'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
64
60
|
requirements: []
|
65
|
-
|
66
61
|
rubyforge_project: etchsyscm
|
67
|
-
rubygems_version: 1.
|
62
|
+
rubygems_version: 1.8.15
|
68
63
|
signing_key:
|
69
64
|
specification_version: 3
|
70
65
|
summary: Etch system configuration management client
|
71
66
|
test_files: []
|
72
|
-
|