konzertmeister 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5679713dea43f8aa152592f629a42b5a7ae79d89
4
+ data.tar.gz: 6bd78152ad09ed1ae8d9daf7f42c124c8b61ed2e
5
+ SHA512:
6
+ metadata.gz: e5888c8b9b3277252faa67d3c259ff22640b4caf88437f250b26a5fdd606941eea6b032bb39f1bf64e4cc48220f8acbeabaa8d1d56bc2fdff232bb3a680809fd
7
+ data.tar.gz: 8d500622431f22d2625ab48e55add264d3841862803a3ea7412fd49b72d4b4a48a8de518f05a33e60f3a6dbbb1672348b940f61c42b49784a71ea04d3ea6166f
@@ -0,0 +1,34 @@
1
+ require 'rest-client'
2
+
3
+ class Konzertmeister
4
+ autoload :Backend, File.expand_path(File.join(File.dirname(__FILE__), 'konzertmeister', 'backend.rb'))
5
+ autoload :Host, File.expand_path(File.join(File.dirname(__FILE__), 'konzertmeister', 'host.rb'))
6
+ autoload :Customer, File.expand_path(File.join(File.dirname(__FILE__), 'konzertmeister', 'customer.rb'))
7
+ autoload :MetaVersion, File.expand_path(File.join(File.dirname(__FILE__), 'konzertmeister', 'meta_version.rb'))
8
+ autoload :InfrastructureVersion, File.expand_path(File.join(File.dirname(__FILE__), 'konzertmeister', 'infrastructure_version.rb'))
9
+ autoload :ProductVersion, File.expand_path(File.join(File.dirname(__FILE__), 'konzertmeister', 'product_version.rb'))
10
+ autoload :Session, File.expand_path(File.join(File.dirname(__FILE__), 'konzertmeister', 'session.rb'))
11
+
12
+ def self.session
13
+ return @@session
14
+ end
15
+
16
+ def self.session=(new_session)
17
+ @@session = new_session
18
+ return @@session
19
+ end
20
+
21
+ def self.new_session(
22
+ hostname: 'konzertmeister',
23
+ port: 443,
24
+ ssl_client_cert: nil,
25
+ ssl_client_key: nil
26
+ )
27
+ @@session = Konzertmeister::Session.new(
28
+ hostname: hostname,
29
+ port: port,
30
+ ssl_client_cert: ssl_client_cert,
31
+ ssl_client_key: ssl_client_key
32
+ )
33
+ end
34
+ end
@@ -0,0 +1,197 @@
1
+ class Konzertmeister
2
+ class Backend
3
+ KINDS = %w[
4
+ production
5
+ sandbox
6
+ development
7
+ internal
8
+ ]
9
+
10
+ attr_accessor :name, :kind
11
+ attr_reader :id, :short_name, :puppet_master, :monthly_cost, :created_at, :updated_at
12
+
13
+ def self.all
14
+ response = Konzertmeister.session.get('/backends')
15
+ if response
16
+ response.map do |data|
17
+ Konzertmeister::Backend.new(data)
18
+ end.sort_by do |b|
19
+ "#{b.customer.short_name} #{b.short_name}"
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.find_by(attr, value)
25
+ Backend.new(Konzertmeister.session.get("/backends/#{attr}/#{value}"))
26
+ end
27
+
28
+ def initialize(data = {})
29
+ @data = data
30
+ data.each do |k,value|
31
+ next if %w[hosts customer meta_version].include?(k)
32
+
33
+ if m = k.match(/^backend_(.*)$/)
34
+ key = m[1]
35
+ else
36
+ key = k
37
+ end
38
+
39
+ instance_variable_set("@#{key}", value)
40
+ end
41
+ end
42
+
43
+ def save
44
+ params = {
45
+ backend_platform: 'Rackspace',
46
+ backend_name: name,
47
+ backend_kind: kind,
48
+ customer: {customer_id: customer.id},
49
+ meta_version: {meta_version_id: meta_version.id},
50
+ coda_count: coda_count,
51
+ coda_flavor: coda_flavor,
52
+ postgres_flavor: postgres_flavor
53
+ }
54
+ puts "Creating with params: #{params}"
55
+
56
+ response = Konzertmeister.session.post("/backends", params)
57
+ if response
58
+ response_object = Konzertmeister::Backend.new(response)
59
+ @id = response_object.id
60
+ @short_name = response_object.short_name
61
+ @puppet_master = response_object.puppet_master
62
+ @monthly_cost = response_object.monthly_cost
63
+ @created_at = response_object.created_at
64
+ @updated_at = response_object.updated_at
65
+ response_object
66
+ end
67
+ end
68
+
69
+ def hosts
70
+ @hosts ||= if @data.key?('hosts')
71
+ @data.fetch('hosts').map do |host_hash|
72
+ Konzertmeister::Host.new(host_hash)
73
+ end.sort_by(&:hostname)
74
+ else
75
+ Backend.find_by('id', self.id).hosts
76
+ end
77
+ end
78
+
79
+ def coda_count
80
+ @coda_count ||= self.hosts.size
81
+ end
82
+
83
+ def coda_count=(size)
84
+ @coda_count = size
85
+ end
86
+
87
+ def coda_flavor
88
+ @coda_flavor ||= self.hosts.find{|h| h.kind == Konzertmeister::Host::CODA_KIND}.flavor
89
+ end
90
+
91
+ def coda_flavor=(input_flavor)
92
+ if Konzertmeister::Host::FLAVORS.include?(input_flavor)
93
+ @coda_flavor = input_flavor
94
+ else
95
+ raise "Invalid flavor: #{input_flavor}. Choose one of: #{Konzertmeister::Host::Flavors.join(', ')}"
96
+ end
97
+ end
98
+
99
+ def postgres_flavor
100
+ @postgres_flavor ||= self.hosts.find{|h| h.kind == Konzertmeister::Host::POSTGRES_KIND}.flavor
101
+ end
102
+
103
+ def postgres_flavor=(input_flavor)
104
+ if Konzertmeister::Host::FLAVORS.include?(input_flavor)
105
+ @postgres_flavor = input_flavor
106
+ else
107
+ raise "Invalid flavor: #{input_flavor}. Choose one of: #{Konzertmeister::Host::Flavors.join(', ')}"
108
+ end
109
+ end
110
+
111
+ def meta_version
112
+ @meta_version ||= if @data.key?('meta_version')
113
+ Konzertmeister::MetaVersion.new(@data.fetch('meta_version'))
114
+ else
115
+ Backend.find_by('id', self.id).meta_version
116
+ end
117
+ end
118
+
119
+ def meta_version=(input_mv)
120
+ @meta_version = input_mv
121
+ end
122
+
123
+ def customer
124
+ @customer ||= if @data.key?('customer')
125
+ Konzertmeister::Customer.new(@data.fetch('customer'))
126
+ else
127
+ Backend.find_by('id', self.id).customer
128
+ end
129
+ end
130
+
131
+ def customer=(input_customer)
132
+ @customer = input_customer
133
+ end
134
+
135
+ def proxy_host
136
+ @proxy_host ||= @data.fetch('proxy').fetch('proxy_host')
137
+ end
138
+
139
+ def proxy_port
140
+ @proxy_port ||= @data.fetch('proxy').fetch('proxy_port')
141
+ end
142
+
143
+ def full_name
144
+ [
145
+ short_name,
146
+ customer.short_name
147
+ ].join("-")
148
+ end
149
+
150
+ def fqdn(scope = nil)
151
+ first = scope.nil? ? full_name : "#{full_name}-admin"
152
+ [
153
+ first,
154
+ "backend",
155
+ "tempoiq",
156
+ "com"
157
+ ].join(".")
158
+ end
159
+
160
+ def production?
161
+ @kind == 'production'
162
+ end
163
+
164
+ def ssh_prefix(user, port=1535, *args)
165
+ proxy = @data.fetch("proxy")
166
+ proxy_host = proxy.fetch("proxy_host")
167
+ proxy_port = proxy.fetch("proxy_port")
168
+
169
+ %Q{ssh -p #{port} -o "ProxyCommand ssh -p #{proxy_port} #{user}@#{proxy_host} nc %h %p" #{args.join(' ')}}
170
+ end
171
+
172
+ def proxy_command(host, user, port=1535, *args)
173
+ prefix = ssh_prefix(user, port, *args)
174
+ %Q{#{prefix} #{user}@#{host.management_ip}}
175
+ end
176
+
177
+ def rsync_up_command(host, user, file_local, file_remote="", port=1535, *args)
178
+ prefix = ssh_prefix(user, port, *args)
179
+ %Q{rsync -avzP -e '#{prefix}' #{file_local} #{user}@#{host.management_ip}:#{file_remote}}
180
+ end
181
+
182
+ def rsync_down_command(host, user, file_remote, file_local, port=1535, *args)
183
+ prefix = ssh_prefix(user, port, *args)
184
+ %Q{rsync -avzP -e '#{prefix}' #{user}@#{host.management_ip}:#{file_remote} #{file_local}}
185
+ end
186
+
187
+ def to_s
188
+ cost = "%1.2f" % monthly_cost
189
+
190
+ ([
191
+ "$#{cost}/month #{full_name} (#{kind})",
192
+ "MetaVersion: #{meta_version}",
193
+ "Hosts:"
194
+ ] + hosts.sort_by(&:name).map {|h| " #{h}"}).join("\n")
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,65 @@
1
+ class Konzertmeister
2
+ class Customer
3
+ attr_accessor :name
4
+ attr_reader :id, :short_name, :created_at, :updated_at
5
+
6
+ def self.all
7
+ response = Konzertmeister.session.get('/customers')
8
+ if response
9
+ response.map do |data|
10
+ Konzertmeister::Customer.new(data)
11
+ end.sort_by(&:short_name)
12
+ end
13
+ end
14
+
15
+ def self.find_by(attr, value)
16
+ Customer.new(Konzertmeister.session.get("/customers/#{attr}/#{value}"))
17
+ end
18
+
19
+ def initialize(data = {})
20
+ @data = data
21
+ data.each do |k,value|
22
+ next if k == 'backends'
23
+
24
+ if m = k.match(/^customer_(.*)$/)
25
+ key = m[1]
26
+ else
27
+ key = k
28
+ end
29
+
30
+ instance_variable_set("@#{key}", value)
31
+ end
32
+ end
33
+
34
+ def save
35
+ params = {
36
+ customer_name: name
37
+ }
38
+ response = Konzertmeister.session.post("/customers", params)
39
+ if response
40
+ response_object = Konzertmeister::Customer.new(response)
41
+ @id = response_object.id
42
+ @short_name = response_object.short_name
43
+ end
44
+ end
45
+
46
+ def backends
47
+ @backends ||= if @data.key?('backends')
48
+ @data.fetch('backends').map do |be_hash|
49
+ Konzertmeister::Backend.new(be_hash)
50
+ end.sort_by(&:short_name)
51
+ else
52
+ Customer.find_by('id', self.id).backends
53
+ end
54
+ end
55
+
56
+ def to_s
57
+ monthly_cost = backends.inject(0) do |sum, backend|
58
+ sum + backend.monthly_cost
59
+ end
60
+
61
+ cost_string = "$%1.2f/month" % [monthly_cost]
62
+ "%-14s %s" % [cost_string, "#{name} (#{short_name})"]
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,66 @@
1
+ class Konzertmeister
2
+ class Host
3
+ FLAVORS = %w[
4
+ 2x_small
5
+ extra_small
6
+ small
7
+ medium
8
+ large
9
+ extra_large
10
+ 2x_large
11
+ ]
12
+ POSTGRES_KIND = 'postgres'
13
+ CODA_KIND = 'coda'
14
+ UTIL_KIND = 'util'
15
+
16
+ attr_reader :id, :hostname, :backend_ip, :management_ip, :kind, :token, :status, :monthly_cost, :created_at, :updated_at, :flavor
17
+
18
+ def self.all
19
+ response = Konzertmeister.session.get('/hosts')
20
+ if response
21
+ response.map do |data|
22
+ Konzertmeister::Host.new(data)
23
+ end.sort_by(&:hostname)
24
+ end
25
+ end
26
+
27
+ def self.find_by(attr, value)
28
+ Host.new(Konzertmeister.session.get("/hosts/#{attr}/#{value}"))
29
+ end
30
+
31
+ def initialize(data)
32
+ @data = data
33
+ data.each do |k,value|
34
+ next if %w[backend].include?(k)
35
+ if m = k.match(/^host_(.*)$/)
36
+ key = m[1]
37
+ else
38
+ key = k
39
+ end
40
+
41
+ instance_variable_set("@#{key}", value)
42
+ end
43
+ end
44
+
45
+ def backend
46
+ @backend ||= if @data.key?('backend')
47
+ Konzertmeister::Backend.new(@data.fetch('backend'))
48
+ else
49
+ Host.find_by('id', self.id).backend
50
+ end
51
+ end
52
+
53
+ def name
54
+ "#{kind}-#{token}"
55
+ end
56
+
57
+ def to_s
58
+ if status == "active"
59
+ cost_string = "$%1.2f/month" % [monthly_cost]
60
+ "%-14s %s" % [cost_string, name]
61
+ else
62
+ "%-14s %s" % ["(inactive)", name]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,65 @@
1
+ class Konzertmeister
2
+ class InfrastructureVersion
3
+ attr_reader :id, :commit, :tag, :branch, :created_at, :updated_at
4
+
5
+ def initialize(data = {})
6
+ @data = data
7
+ data.each do |k,value|
8
+ if m = k.match(/^infrastructure_version_id$/)
9
+ key = "id"
10
+ else
11
+ key = k
12
+ end
13
+
14
+ instance_variable_set("@#{key}", value)
15
+ end
16
+ end
17
+
18
+ def save
19
+ if commit
20
+ params = { 'commit' => commit }
21
+ elsif tag
22
+ params = { 'tag' => tag }
23
+ elsif branch
24
+ params = { 'branch' => branch }
25
+ end
26
+
27
+ response = Konzertmeister.session.post('/infrastructure_versions', params)
28
+ if response
29
+ response_object = Konzertmeister::InfrastructureVersion.new(response)
30
+ @id = response_object.id
31
+ response_object
32
+ end
33
+ end
34
+
35
+ def commit=(input_commit)
36
+ remove_instance_variable(:@tag) if instance_variable_defined?(:@tag)
37
+ remove_instance_variable(:@branch) if instance_variable_defined?(:@branch)
38
+ @commit = input_commit
39
+ end
40
+
41
+ def tag=(input_tag)
42
+ remove_instance_variable(:@branch) if instance_variable_defined?(:@branch)
43
+ remove_instance_variable(:@commit) if instance_variable_defined?(:@commit)
44
+ @tag = input_tag
45
+ end
46
+
47
+ def branch=(input_branch)
48
+ remove_instance_variable(:@tag) if instance_variable_defined?(:@tag)
49
+ remove_instance_variable(:@commit) if instance_variable_defined?(:@commit)
50
+ @branch = input_branch
51
+ end
52
+
53
+ def to_s
54
+ if @data.has_key?("commit")
55
+ inf_version_string = "commit:#{@data["commit"]}"
56
+ elsif @data.has_key?("tag")
57
+ inf_version_string = "tag:#{@data["tag"]}"
58
+ else
59
+ inf_version_string = "branch:#{@data["branch"]}"
60
+ end
61
+
62
+ "Inf<#{inf_version_string}>"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,109 @@
1
+ class Konzertmeister
2
+ class MetaVersion
3
+ attr_reader :id, :created_at, :updated_at
4
+
5
+ def self.all
6
+ response = Konzertmeister.session.get('/meta_versions')
7
+ if response
8
+ response.map do |data|
9
+ Konzertmeister::MetaVersion.new(data)
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.find_by(attr, value)
15
+ MetaVersion.new(Konzertmeister.session.get("/meta_versions/#{attr}/#{value}"))
16
+ end
17
+
18
+ def initialize(data = {})
19
+ @data = data
20
+ data.each do |k,value|
21
+ next if %w[
22
+ infrastructure_version
23
+ tapp_version
24
+ legato_version
25
+ forte_version
26
+ backends
27
+ ].include?(k)
28
+
29
+ if m = k.match(/^meta_version_(.*)$/)
30
+ key = m[1]
31
+ else
32
+ key = k
33
+ end
34
+
35
+ instance_variable_set("@#{key}", value)
36
+ end
37
+ end
38
+
39
+ def save
40
+ params = {
41
+ 'infrastructure_version' => {'infrastructure_version_id' => infrastructure_version.id},
42
+ 'tapp_version' => {'tapp_version_id' => tapp_version.id},
43
+ 'legato_version' => {'legato_version_id' => legato_version.id},
44
+ 'forte_version' => {'forte_version_id' => forte_version.id}
45
+ }
46
+ response = Konzertmeister.session.post('/meta_versions', params)
47
+ if response
48
+ response_object = Konzertmeister::MetaVersion.new(response)
49
+ @id = response_object.id
50
+ response_object
51
+ end
52
+ end
53
+
54
+ def infrastructure_version
55
+ @infrastructure_version ||= Konzertmeister::InfrastructureVersion.new(@data.fetch("infrastructure_version"))
56
+ end
57
+
58
+ def infrastructure_version=(input_iv)
59
+ @infrastructure_version = input_iv
60
+ end
61
+
62
+ def tapp_version
63
+ @tapp_version ||= Konzertmeister::ProductVersion.new("tapp", @data.fetch("tapp_version"))
64
+ end
65
+
66
+ def tapp_version=(input_tv)
67
+ @tapp_version = input_tv
68
+ end
69
+
70
+ def legato_version
71
+ @legato_version ||= Konzertmeister::ProductVersion.new("legato", @data.fetch("legato_version"))
72
+ end
73
+
74
+ def legato_version=(input_lv)
75
+ @legato_version = input_lv
76
+ end
77
+
78
+ def forte_version
79
+ @forte_version ||= Konzertmeister::ProductVersion.new("forte", @data.fetch("forte_version"))
80
+ end
81
+
82
+ def forte_version=(input_fv)
83
+ @forte_version = input_fv
84
+ end
85
+
86
+ def to_s(format = :short)
87
+ fields = [
88
+ id,
89
+ infrastructure_version,
90
+ tapp_version,
91
+ legato_version,
92
+ forte_version
93
+ ]
94
+ table_widths = "%-3s %-25s %-20s %-25s %-25s"
95
+
96
+ if @data.has_key?("backends")
97
+ fields << "Backends: #{@data.fetch("backends").size}"
98
+ table_widths += " %-11s"
99
+ end
100
+
101
+ case format
102
+ when :short
103
+ fields.join(" ")
104
+ when :table
105
+ table_widths % fields
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,33 @@
1
+ class Konzertmeister
2
+ class ProductVersion
3
+ attr_accessor :version
4
+ attr_reader :id, :product, :created_at, :updated_at
5
+
6
+ def initialize(product, data = {})
7
+ @product = product
8
+ @data = data
9
+ data.each do |k,value|
10
+ if m = k.match(/^(tapp|legato|forte)_version_id$/)
11
+ key = "id"
12
+ else
13
+ key = k
14
+ end
15
+
16
+ instance_variable_set("@#{key}", value)
17
+ end
18
+ end
19
+
20
+ def save
21
+ response = Konzertmeister.session.post("/#{product}_versions", {'version' => version})
22
+ if response
23
+ response_object = Konzertmeister::ProductVersion.new(product, response)
24
+ @id = response_object.id
25
+ response_object
26
+ end
27
+ end
28
+
29
+ def to_s
30
+ "#{@product.capitalize}<#{version}>"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,74 @@
1
+ require 'rest-client'
2
+
3
+ class Konzertmeister
4
+ class Session
5
+
6
+ def initialize(
7
+ hostname: 'konzertmeister',
8
+ port: 443,
9
+ ssl_client_cert: nil,
10
+ ssl_client_key: nil
11
+ )
12
+ @ssl_client_cert = OpenSSL::X509::Certificate.new(File.read(ssl_client_cert)) unless ssl_client_cert.nil?
13
+ @ssl_client_key = OpenSSL::PKey::RSA.new(File.read(ssl_client_key)) unless ssl_client_key.nil?
14
+ @hostname = hostname
15
+ @port = port
16
+ end
17
+
18
+ def get(path, headers = {}, &block)
19
+ url = _url(path)
20
+ params = _request_params(
21
+ :method => :get,
22
+ :url => url,
23
+ :headers => headers,
24
+ :timeout => nil
25
+ )
26
+ begin
27
+ json = RestClient::Request.execute(params, &block)
28
+ JSON.parse(json)
29
+ rescue => e
30
+ puts "ERROR"
31
+ p e
32
+ false
33
+ end
34
+ end
35
+
36
+ def post(path, params, headers = {}, &block)
37
+ json = JSON.generate(params)
38
+ url = _url(path)
39
+ request_params = _request_params({
40
+ :method => :post,
41
+ :url => url,
42
+ :payload => json,
43
+ :headers => headers,
44
+ :timeout => nil
45
+ })
46
+ begin
47
+ response = RestClient::Request.execute(request_params, &block)
48
+ JSON.parse(response)
49
+ rescue RestClient::Exception => e
50
+ puts
51
+ puts "ERROR making request to #{path}:"
52
+ p e
53
+ false
54
+ end
55
+ end
56
+
57
+ def _request_params(extra = {})
58
+ base = {}
59
+ base[:ssl_client_cert] = @ssl_client_cert if @ssl_client_cert
60
+ base[:ssl_client_key] = @ssl_client_key if @ssl_client_key
61
+
62
+ base.merge(extra)
63
+ end
64
+
65
+ def _url(path = nil)
66
+ scheme = (@port == 443 ? "https" : "http")
67
+ [
68
+ "#{scheme}://#{@hostname}:#{@port}",
69
+ path
70
+ ].compact.join("")
71
+ end
72
+
73
+ end
74
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: konzertmeister
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - TempoIQ Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ description: This is the client code for an internal book-keeping and cloud orchestration
28
+ service named Konzertmeister.
29
+ email:
30
+ - service@tempoiq.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - lib/konzertmeister.rb
36
+ - lib/konzertmeister/backend.rb
37
+ - lib/konzertmeister/customer.rb
38
+ - lib/konzertmeister/host.rb
39
+ - lib/konzertmeister/infrastructure_version.rb
40
+ - lib/konzertmeister/meta_version.rb
41
+ - lib/konzertmeister/product_version.rb
42
+ - lib/konzertmeister/session.rb
43
+ homepage: http://tempoiq.com
44
+ licenses: []
45
+ metadata: {}
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 2.2.2
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Konzertmeister http client
66
+ test_files: []