opentox-ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +674 -0
- data/README.rdoc +23 -0
- data/Rakefile +87 -0
- data/VERSION +1 -0
- data/bin/opentox-install-debian.sh +105 -0
- data/bin/opentox-install-ubuntu.sh +375 -0
- data/lib/algorithm.rb +82 -0
- data/lib/compound.rb +128 -0
- data/lib/config/config_ru.rb +51 -0
- data/lib/dataset.rb +226 -0
- data/lib/environment.rb +77 -0
- data/lib/helper.rb +26 -0
- data/lib/model.rb +143 -0
- data/lib/opentox.owl +809 -0
- data/lib/overwrite.rb +14 -0
- data/lib/rest_client_wrapper.rb +168 -0
- data/lib/spork.rb +83 -0
- data/lib/task.rb +176 -0
- data/lib/templates/config.yaml +41 -0
- data/lib/validation.rb +20 -0
- metadata +437 -0
data/lib/overwrite.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# class overwrites aka monkey patches
|
2
|
+
# hack: store sinatra in global var to make url_for and halt methods accessible
|
3
|
+
before{ $sinatra = self unless $sinatra }
|
4
|
+
|
5
|
+
class Sinatra::Base
|
6
|
+
# overwriting halt to log halts (!= 202)
|
7
|
+
def halt(*response)
|
8
|
+
LOGGER.error "halt "+response.first.to_s+" "+(response.size>1 ? response[1].to_s : "") if response and response.first and response.first >= 300
|
9
|
+
# orig sinatra code:
|
10
|
+
response = response.first if response.length == 1
|
11
|
+
throw :halt, response
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,168 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module OpenTox
|
4
|
+
|
5
|
+
#PENDING: implement ot error api, move to own file
|
6
|
+
class Error
|
7
|
+
|
8
|
+
attr_accessor :code, :body, :uri, :payload, :headers
|
9
|
+
|
10
|
+
def initialize(code, body, uri, payload, headers)
|
11
|
+
self.code = code
|
12
|
+
self.body = body.to_s[0..1000]
|
13
|
+
self.uri = uri
|
14
|
+
self.payload = payload
|
15
|
+
self.headers = headers
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.parse(error_array_string)
|
19
|
+
begin
|
20
|
+
err = YAML.load(error_array_string)
|
21
|
+
if err and err.is_a?(Array) and err.size>0 and err[0].is_a?(Error)
|
22
|
+
return err
|
23
|
+
else
|
24
|
+
return nil
|
25
|
+
end
|
26
|
+
rescue
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
class WrapperResult < String
|
34
|
+
attr_accessor :content_type, :code
|
35
|
+
end
|
36
|
+
|
37
|
+
class RestClientWrapper
|
38
|
+
|
39
|
+
def self.get(uri, headers=nil, wait=true)
|
40
|
+
execute( "get", uri, headers, nil, wait)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.post(uri, headers, payload=nil, wait=true)
|
44
|
+
execute( "post", uri, headers, payload, wait )
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.put(uri, headers, payload=nil )
|
48
|
+
execute( "put", uri, headers, payload )
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.delete(uri, headers=nil)
|
52
|
+
execute( "delete", uri, headers, nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.raise_uri_error(error_msg, uri, headers=nil, payload=nil)
|
56
|
+
do_halt( "-", error_msg, uri, headers, payload )
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def self.execute( rest_call, uri, headers, payload=nil, wait=true )
|
61
|
+
|
62
|
+
do_halt 400,"uri is null",uri,headers,payload unless uri
|
63
|
+
do_halt 400,"not a uri",uri,headers,payload unless Utils.is_uri?(uri)
|
64
|
+
do_halt 400,"headers are no hash",uri,headers,payload unless headers==nil or headers.is_a?(Hash)
|
65
|
+
do_halt 400,"nil headers for post not allowed, use {}",uri,headers,payload if rest_call=="post" and headers==nil
|
66
|
+
headers.each{ |k,v| headers.delete(k) if v==nil } if headers #remove keys with empty values, as this can cause problems
|
67
|
+
|
68
|
+
begin
|
69
|
+
#LOGGER.debug "RestCall: "+rest_call.to_s+" "+uri.to_s+" "+headers.inspect
|
70
|
+
resource = RestClient::Resource.new(uri,{:timeout => 60}) #, :user => @@users[:users].keys[0], :password => @@users[:users].values[0]})
|
71
|
+
if payload
|
72
|
+
result = resource.send(rest_call, payload, headers)
|
73
|
+
elsif headers
|
74
|
+
result = resource.send(rest_call, headers)
|
75
|
+
else
|
76
|
+
result = resource.send(rest_call)
|
77
|
+
end
|
78
|
+
|
79
|
+
# result is a string, with the additional fields content_type and code
|
80
|
+
res = WrapperResult.new(result.body)
|
81
|
+
res.content_type = result.headers[:content_type]
|
82
|
+
raise "content-type not set" unless res.content_type
|
83
|
+
res.code = result.code
|
84
|
+
|
85
|
+
return res if res.code==200 || !wait
|
86
|
+
|
87
|
+
while (res.code==201 || res.code==202)
|
88
|
+
res = wait_for_task(res, uri)
|
89
|
+
end
|
90
|
+
raise "illegal status code: '"+res.code.to_s+"'" unless res.code==200
|
91
|
+
return res
|
92
|
+
|
93
|
+
rescue RestClient::RequestTimeout => ex
|
94
|
+
do_halt 408,ex.message,uri,headers,payload
|
95
|
+
rescue => ex
|
96
|
+
#raise ex
|
97
|
+
#raise "'"+ex.message+"' uri: "+uri.to_s
|
98
|
+
begin
|
99
|
+
code = ex.http_code
|
100
|
+
msg = ex.http_body
|
101
|
+
rescue
|
102
|
+
code = 500
|
103
|
+
msg = ex.to_s
|
104
|
+
end
|
105
|
+
do_halt code,msg,uri,headers,payload
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.wait_for_task( res, base_uri )
|
110
|
+
|
111
|
+
task = nil
|
112
|
+
case res.content_type
|
113
|
+
when /application\/rdf\+xml|application\/x-yaml/
|
114
|
+
task = OpenTox::Task.from_data(res, res.content_type, res.code, base_uri)
|
115
|
+
when /text\//
|
116
|
+
raise "uri list has more than one entry, should be a task" if res.content_type=~/text\/uri-list/ and
|
117
|
+
res.split("\n").size > 1 #if uri list contains more then one uri, its not a task
|
118
|
+
task = OpenTox::Task.find(res.to_s) if Utils.task_uri?(res)
|
119
|
+
else
|
120
|
+
raise "unknown content-type for task: '"+res.content_type.to_s+"'" #+"' content: "+res[0..200].to_s
|
121
|
+
end
|
122
|
+
|
123
|
+
LOGGER.debug "result is a task '"+task.uri.to_s+"', wait for completion"
|
124
|
+
task.wait_for_completion
|
125
|
+
raise task.description unless task.completed? # maybe task was cancelled / error
|
126
|
+
|
127
|
+
res = WrapperResult.new task.resultURI
|
128
|
+
res.code = task.http_code
|
129
|
+
res.content_type = "text/uri-list"
|
130
|
+
return res
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.do_halt( code, body, uri, headers, payload=nil )
|
134
|
+
|
135
|
+
#build error
|
136
|
+
causing_errors = Error.parse(body)
|
137
|
+
if causing_errors
|
138
|
+
error = causing_errors + [Error.new(code, "subsequent error", uri, payload, headers)]
|
139
|
+
else
|
140
|
+
error = [Error.new(code, body, uri, payload, headers)]
|
141
|
+
end
|
142
|
+
|
143
|
+
#debug utility: write error to file
|
144
|
+
error_dir = "/tmp/ot_errors"
|
145
|
+
FileUtils.mkdir(error_dir) unless File.exist?(error_dir)
|
146
|
+
raise "could not create error dir" unless File.exist?(error_dir) and File.directory?(error_dir)
|
147
|
+
file_name = "error"
|
148
|
+
time=Time.now.strftime("%m.%d.%Y-%H:%M:%S")
|
149
|
+
count = 1
|
150
|
+
count+=1 while File.exist?(File.join(error_dir,file_name+"_"+time+"_"+count.to_s))
|
151
|
+
File.new(File.join(error_dir,file_name+"_"+time+"_"+count.to_s),"w").puts(body)
|
152
|
+
|
153
|
+
# handle error
|
154
|
+
# we are either in a task, or in sinatra
|
155
|
+
# PENDING: always return yaml for now
|
156
|
+
|
157
|
+
if $self_task #this global var in Task.as_task to mark that the current process is running in a task
|
158
|
+
raise error.to_yaml # the error is caught, logged, and task state is set to error in Task.as_task
|
159
|
+
#elsif $sinatra #else halt sinatra
|
160
|
+
#$sinatra.halt(502,error.to_yaml)
|
161
|
+
elsif defined?(halt)
|
162
|
+
halt(502,error.to_yaml)
|
163
|
+
else #for testing purposes (if classes used directly)
|
164
|
+
raise error.to_yaml
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/lib/spork.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# A way to cleanly handle process forking in Sinatra when using Passenger, aka "sporking some code".
|
2
|
+
# This will allow you to properly execute some code asynchronously, which otherwise does not work correctly.
|
3
|
+
#
|
4
|
+
# Written by Ron Evans
|
5
|
+
# More info at http://deadprogrammersociety.com
|
6
|
+
#
|
7
|
+
# Mostly lifted from the Spawn plugin for Rails (http://github.com/tra/spawn)
|
8
|
+
# but with all of the Rails stuff removed.... cause you are using Sinatra. If you are using Rails, Spawn is
|
9
|
+
# what you need. If you are using something else besides Sinatra that is Rack-based under Passenger, and you are having trouble with
|
10
|
+
# asynch processing, let me know if spork helped you.
|
11
|
+
#
|
12
|
+
module Spork
|
13
|
+
# things to close in child process
|
14
|
+
@@resources = []
|
15
|
+
def self.resources
|
16
|
+
@@resources
|
17
|
+
end
|
18
|
+
|
19
|
+
# set the resource to disconnect from in the child process (when forking)
|
20
|
+
def self.resource_to_close(resource)
|
21
|
+
@@resources << resource
|
22
|
+
end
|
23
|
+
|
24
|
+
# close all the resources added by calls to resource_to_close
|
25
|
+
def self.close_resources
|
26
|
+
@@resources.each do |resource|
|
27
|
+
resource.close if resource && resource.respond_to?(:close) && !resource.closed?
|
28
|
+
end
|
29
|
+
@@resources = []
|
30
|
+
end
|
31
|
+
|
32
|
+
# actually perform the fork... er, spork
|
33
|
+
# valid options are:
|
34
|
+
# :priority => to set the process priority of the child
|
35
|
+
# :logger => a logger object to use from the child
|
36
|
+
# :no_detach => true if you want to keep the child process under the parent control. usually you do NOT want this
|
37
|
+
def self.spork(options={})
|
38
|
+
logger = options[:logger]
|
39
|
+
logger.debug "spork> parent PID = #{Process.pid}" if logger
|
40
|
+
|
41
|
+
child = fork do
|
42
|
+
begin
|
43
|
+
start = Time.now
|
44
|
+
logger.debug "spork> child PID = #{Process.pid}" if logger
|
45
|
+
|
46
|
+
# set the nice priority if needed
|
47
|
+
Process.setpriority(Process::PRIO_PROCESS, 0, options[:priority]) if options[:priority]
|
48
|
+
|
49
|
+
# disconnect from the rack
|
50
|
+
Spork.close_resources
|
51
|
+
|
52
|
+
# run the block of code that takes so long
|
53
|
+
yield
|
54
|
+
|
55
|
+
rescue => ex
|
56
|
+
#raise ex
|
57
|
+
logger.error "spork> Exception in child[#{Process.pid}] - #{ex.class}: #{ex.message}" if logger
|
58
|
+
ensure
|
59
|
+
logger.info "spork> child[#{Process.pid}] took #{Time.now - start} sec" if logger
|
60
|
+
# this form of exit doesn't call at_exit handlers
|
61
|
+
exit!(0)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# detach from child process (parent may still wait for detached process if they wish)
|
66
|
+
Process.detach(child) unless options[:no_detach]
|
67
|
+
|
68
|
+
return child
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
# Patch to work with passenger
|
74
|
+
if defined? Passenger::Rack::RequestHandler
|
75
|
+
class Passenger::Rack::RequestHandler
|
76
|
+
alias_method :orig_process_request, :process_request
|
77
|
+
def process_request(env, input, output)
|
78
|
+
Spork.resource_to_close(input)
|
79
|
+
Spork.resource_to_close(output)
|
80
|
+
orig_process_request(env, input, output)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/task.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
$self_task=nil
|
2
|
+
|
3
|
+
module OpenTox
|
4
|
+
|
5
|
+
class Task
|
6
|
+
|
7
|
+
# due_to_time is only set in local tasks
|
8
|
+
TASK_ATTRIBS = [ :uri, :date, :title, :creator, :description, :hasStatus, :percentageCompleted, :resultURI, :due_to_time ]
|
9
|
+
TASK_ATTRIBS.each{ |a| attr_accessor(a) }
|
10
|
+
attr_accessor :http_code
|
11
|
+
|
12
|
+
private
|
13
|
+
def initialize(uri)
|
14
|
+
@uri = uri.to_s.strip
|
15
|
+
end
|
16
|
+
|
17
|
+
# create is private now, use OpenTox::Task.as_task
|
18
|
+
def self.create( params )
|
19
|
+
task_uri = RestClientWrapper.post(@@config[:services]["opentox-task"], params, nil, false).to_s
|
20
|
+
Task.find(task_uri.chomp)
|
21
|
+
end
|
22
|
+
|
23
|
+
public
|
24
|
+
def self.find( uri, accept_header=nil )
|
25
|
+
task = Task.new(uri)
|
26
|
+
task.reload( accept_header )
|
27
|
+
return task
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_data(data, content_type, code, base_uri)
|
31
|
+
task = Task.new(nil)
|
32
|
+
task.http_code = code
|
33
|
+
task.reload_from_data(data, content_type, base_uri)
|
34
|
+
return task
|
35
|
+
end
|
36
|
+
|
37
|
+
def reload( accept_header=nil )
|
38
|
+
unless accept_header
|
39
|
+
if (@@config[:yaml_hosts].include?(URI.parse(uri).host))
|
40
|
+
accept_header = "application/x-yaml"
|
41
|
+
else
|
42
|
+
accept_header = 'application/rdf+xml'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
result = RestClientWrapper.get(uri, {:accept => accept_header}, false)#'application/x-yaml'})
|
46
|
+
@http_code = result.code
|
47
|
+
reload_from_data(result, result.content_type, uri)
|
48
|
+
end
|
49
|
+
|
50
|
+
def reload_from_data( data, content_type, base_uri )
|
51
|
+
case content_type
|
52
|
+
when /yaml/
|
53
|
+
task = YAML.load data
|
54
|
+
TASK_ATTRIBS.each do |a|
|
55
|
+
raise "task yaml data invalid, key missing: "+a.to_s unless task.has_key?(a)
|
56
|
+
send("#{a.to_s}=".to_sym,task[a])
|
57
|
+
end
|
58
|
+
when /application\/rdf\+xml/
|
59
|
+
owl = OpenTox::Owl.from_data(data,base_uri,"Task")
|
60
|
+
self.uri = owl.uri
|
61
|
+
(TASK_ATTRIBS-[:uri]).each{|a| self.send("#{a.to_s}=".to_sym, owl.get(a.to_s))}
|
62
|
+
else
|
63
|
+
raise "content type for tasks not supported: "+content_type.to_s
|
64
|
+
end
|
65
|
+
raise "uri is null after loading" unless @uri and @uri.to_s.strip.size>0
|
66
|
+
end
|
67
|
+
|
68
|
+
def cancel
|
69
|
+
RestClientWrapper.put(File.join(@uri,'Cancelled'))
|
70
|
+
reload
|
71
|
+
end
|
72
|
+
|
73
|
+
def completed(uri)
|
74
|
+
RestClientWrapper.put(File.join(@uri,'Completed'),{:resultURI => uri})
|
75
|
+
reload
|
76
|
+
end
|
77
|
+
|
78
|
+
def error(description)
|
79
|
+
RestClientWrapper.put(File.join(@uri,'Error'),{:description => description.to_s[0..2000]})
|
80
|
+
reload
|
81
|
+
end
|
82
|
+
|
83
|
+
def pid=(pid)
|
84
|
+
RestClientWrapper.put(File.join(@uri,'pid'), {:pid => pid})
|
85
|
+
end
|
86
|
+
|
87
|
+
def running?
|
88
|
+
@hasStatus.to_s == 'Running'
|
89
|
+
end
|
90
|
+
|
91
|
+
def completed?
|
92
|
+
@hasStatus.to_s == 'Completed'
|
93
|
+
end
|
94
|
+
|
95
|
+
def error?
|
96
|
+
@hasStatus.to_s == 'Error'
|
97
|
+
end
|
98
|
+
|
99
|
+
# waits for a task, unless time exceeds or state is no longer running
|
100
|
+
def wait_for_completion(dur=0.3)
|
101
|
+
|
102
|
+
if (@uri.match(@@config[:services]["opentox-task"]))
|
103
|
+
due_to_time = (@due_to_time.is_a?(Time) ? @due_to_time : Time.parse(@due_to_time))
|
104
|
+
running_time = due_to_time - (@date.is_a?(Time) ? @date : Time.parse(@date))
|
105
|
+
else
|
106
|
+
# the date of the external task cannot be trusted, offest to local time might be to big
|
107
|
+
due_to_time = Time.new + EXTERNAL_TASK_MAX_DURATION
|
108
|
+
running_time = EXTERNAL_TASK_MAX_DURATION
|
109
|
+
end
|
110
|
+
LOGGER.debug "start waiting for task "+@uri.to_s+" at: "+Time.new.to_s+", waiting at least until "+due_to_time.to_s
|
111
|
+
|
112
|
+
while self.running?
|
113
|
+
sleep dur
|
114
|
+
reload
|
115
|
+
check_state
|
116
|
+
if (Time.new > due_to_time)
|
117
|
+
raise "max wait time exceeded ("+running_time.to_s+"sec), task: '"+@uri.to_s+"'"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
LOGGER.debug "Task '"+@hasStatus+"': "+@uri.to_s+", Result: "+@resultURI.to_s
|
122
|
+
end
|
123
|
+
|
124
|
+
def check_state
|
125
|
+
begin
|
126
|
+
raise "illegal task state, task is completed, resultURI is no URI: '"+@resultURI.to_s+
|
127
|
+
"'" unless @resultURI and Utils.is_uri?(@resultURI) if completed?
|
128
|
+
|
129
|
+
if @http_code == 202
|
130
|
+
raise "illegal task state, code is 202, but hasStatus is not Running: '"+@hasStatus+"'" unless running?
|
131
|
+
elsif @http_code == 201
|
132
|
+
raise "illegal task state, code is 201, but hasStatus is not Completed: '"+@hasStatus+"'" unless completed?
|
133
|
+
raise "illegal task state, code is 201, resultURI is no task-URI: '"+@resultURI.to_s+
|
134
|
+
"'" unless @resultURI and Utils.task_uri?(@resultURI)
|
135
|
+
end
|
136
|
+
rescue => ex
|
137
|
+
RestClientWrapper.raise_uri_error(ex.message, @uri)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# returns the task uri
|
142
|
+
# catches halts and exceptions, task state is set to error then
|
143
|
+
def self.as_task( title, creator, max_duration=DEFAULT_TASK_MAX_DURATION, description=nil )
|
144
|
+
#return yield nil
|
145
|
+
|
146
|
+
params = {:title=>title, :creator=>creator, :max_duration=>max_duration, :description=>description }
|
147
|
+
task = OpenTox::Task.create(params)
|
148
|
+
task_pid = Spork.spork(:logger => LOGGER) do
|
149
|
+
LOGGER.debug "Task #{task.uri} started #{Time.now}"
|
150
|
+
$self_task = task
|
151
|
+
|
152
|
+
begin
|
153
|
+
result = catch(:halt) do
|
154
|
+
yield task
|
155
|
+
end
|
156
|
+
# catching halt, set task state to error
|
157
|
+
if result && result.is_a?(Array) && result.size==2 && result[0]>202
|
158
|
+
LOGGER.error "task was halted: "+result.inspect
|
159
|
+
task.error(result[1])
|
160
|
+
return
|
161
|
+
end
|
162
|
+
LOGGER.debug "Task #{task.uri} done #{Time.now} -> "+result.to_s
|
163
|
+
task.completed(result)
|
164
|
+
rescue => ex
|
165
|
+
LOGGER.error "task failed: "+ex.message
|
166
|
+
LOGGER.error ": "+ex.backtrace.join("\n")
|
167
|
+
task.error(ex.message)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
task.pid = task_pid
|
171
|
+
LOGGER.debug "Started task: "+task.uri.to_s
|
172
|
+
task.uri
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Example configuration for OpenTox, please adjust to your settings
|
2
|
+
#
|
3
|
+
# Database setup:
|
4
|
+
#
|
5
|
+
# Example MySql:
|
6
|
+
#
|
7
|
+
:database:
|
8
|
+
:adapter: mysql
|
9
|
+
:database: production
|
10
|
+
:username: root
|
11
|
+
:password: opentox
|
12
|
+
:host: localhost
|
13
|
+
#
|
14
|
+
# Example 1: Using external test services
|
15
|
+
#
|
16
|
+
# :services:
|
17
|
+
# opentox-compound: "http://webservices.in-silico.ch/compound/"
|
18
|
+
# opentox-dataset: "http://webservices.in-silico.ch/dataset/"
|
19
|
+
# opentox-algorithm: "http://webservices.in-silico.ch/algorithm/"
|
20
|
+
# opentox-model: "http://webservices.in-silico.ch/model/"
|
21
|
+
# opentox-task: "http://webservices.in-silico.ch/task/"
|
22
|
+
# opentox-validation: "http://opentox.informatik.uni-freiburg.de/validation/"
|
23
|
+
#
|
24
|
+
# Example 2: Using local services
|
25
|
+
:base_dir: /home/ist/webservices
|
26
|
+
:webserver: passenger
|
27
|
+
:services:
|
28
|
+
opentox-compound: "http://localhost/compound/"
|
29
|
+
opentox-dataset: "http://localhost/dataset/"
|
30
|
+
opentox-algorithm: "http://localhost/algorithm/"
|
31
|
+
opentox-model: "http://localhost/model/"
|
32
|
+
opentox-task: "http://localhost/task/"
|
33
|
+
opentox-validation: "http://localhost/validation/"
|
34
|
+
#
|
35
|
+
# Yaml capable hosts (faster than OWL-DL)
|
36
|
+
#
|
37
|
+
:yaml_hosts:
|
38
|
+
- "localhost"
|
39
|
+
|
40
|
+
# Uncomment for verbose logging
|
41
|
+
# :logger: debug
|