ood_core 0.0.4 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8002c2e879b001e56001b19968b7832083ed9bef
4
- data.tar.gz: 4138c0a93735760e1ebaba489231229879f7b72b
3
+ metadata.gz: bab7efc85cb79ee4fbec84bd148968b5e3e7efa4
4
+ data.tar.gz: 856dbe1ae2f138f409875b6b19e007129d4ece73
5
5
  SHA512:
6
- metadata.gz: e308714bcb389500342e493a914542b9c59b63a0a8776ec4dc6883a3ab7dc1696ef44fdee40ea2c74ae9b94c89d8d9e58336cd6f5cf252b403ba055f755f7841
7
- data.tar.gz: f4b67e63a34642884b5bd6de6ecab47484d377b639fc174eb45c5bc11f4cc993e89105bb3092dd7cf6695d27951618ee4fc8f7b41308e6b3a8be8386243e39d2
6
+ metadata.gz: 6d9b87b7134e3e72fff1191ac24658a5243d71cee4d1942a1f5a946345640b4f6a999a7b662db9924c137234183cbef5eafbd355bbb1fe1ee0249a91b32c3866
7
+ data.tar.gz: a958afc1560a75e358f9655df0342a9831e49a2a4d3658bf74878fc61ae7a135a34248cd7b974dd5bea4190129481bb7f2810bf24a38d01fb12c6a64a74a9641
data/CHANGELOG.md CHANGED
@@ -1,33 +1,76 @@
1
- ## Unreleased
1
+ # Changelog
2
2
 
3
- ## 0.0.4 (2017-05-17)
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- Features:
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
7
 
7
- - removed `OodCore::Job::Script#min_phys_memory` due to lack of commonality
8
- across resource managers
9
- - removed `OodCore::Job::Script#join_files` due to lack of support in
10
- resource managers
11
- - by default all PBS jobs output stdout & stderr to output path unless an
12
- error path is specified (mimics behavior of Slurm and LSF)
8
+ ## [Unreleased]
13
9
 
14
- ## 0.0.3 (2017-04-28)
10
+ ## [0.0.5] - 2017-07-05
15
11
 
16
- Features:
12
+ ### Added
17
13
 
18
- - provide support for slurm conf file
14
+ - Add wallclock time limit to `OodCore::Job::Info` object.
15
+ - Add further support for the LSF adapter.
16
+ - Add a new Batch Connect template feature that builds batch scripts to launch
17
+ web servers.
18
+ - Add support for the PBS Professional resource manager.
19
+ - Add method to filter list of batch jobs for a given owner or owners.
19
20
 
20
- Bugfixes:
21
+ ### Changed
21
22
 
22
- - correct code documentation for `Script#min_phys_memory`
23
- - fix for login feature being allowed on all clusters even if not defined
23
+ - Torque adapter provides nodes/procs info if available for non-running jobs.
24
+ - Slurm adapter provides node info if available for non-running jobs.
25
+ - Changed the `CHANGELOG.md` formatting.
24
26
 
25
- ## 0.0.2 (2017-04-27)
27
+ ### Removed
26
28
 
27
- Features:
29
+ - Remove deprecated tests for the Slurm adapter.
28
30
 
29
- - removed the `OodCore::Job::NodeRequest` object
31
+ ### Fixed
30
32
 
31
- ## 0.0.1 (2017-04-17)
33
+ - Fix parsing bjobs output for LSF 9.1, which has extra SLOTS column.
32
34
 
33
- Initial release!
35
+ ## [0.0.4] - 2017-05-17
36
+
37
+ ### Changed
38
+
39
+ - By default all PBS jobs output stdout & stderr to output path unless an error
40
+ path is specified (mimics behavior of Slurm and LSF)
41
+
42
+ ### Removed
43
+
44
+ - Remove `OodCore::Job::Script#min_phys_memory` due to lack of commonality
45
+ across resource managers.
46
+ - Remove `OodCore::Job::Script#join_files` due to lack of support in resource
47
+ managers.
48
+
49
+ ## [0.0.3] - 2017-04-28
50
+
51
+ ### Added
52
+
53
+ - Provide support for Slurm conf file.
54
+
55
+ ### Fixed
56
+
57
+ - Correct code documentation for `Script#min_phys_memory`.
58
+ - Add fix for login feature being allowed on all clusters even if not defined.
59
+
60
+ ## [0.0.2] - 2017-04-27
61
+
62
+ ### Removed
63
+
64
+ - Remove the `OodCore::Job::NodeRequest` object.
65
+
66
+ ## 0.0.1 - 2017-04-17
67
+
68
+ ### Added
69
+
70
+ - Initial release!
71
+
72
+ [Unreleased]: https://github.com/OSC/ood_core/compare/v0.0.5...HEAD
73
+ [0.0.5]: https://github.com/OSC/ood_core/compare/v0.0.4...v0.0.5
74
+ [0.0.4]: https://github.com/OSC/ood_core/compare/v0.0.3...v0.0.4
75
+ [0.0.3]: https://github.com/OSC/ood_core/compare/v0.0.2...v0.0.3
76
+ [0.0.2]: https://github.com/OSC/ood_core/compare/v0.0.1...v0.0.2
data/lib/ood_core.rb CHANGED
@@ -30,4 +30,10 @@ module OodCore
30
30
  module Adapters
31
31
  end
32
32
  end
33
+
34
+ # A namespace for batch connect code
35
+ module BatchConnect
36
+ require "ood_core/batch_connect/template"
37
+ require "ood_core/batch_connect/factory"
38
+ end
33
39
  end
@@ -0,0 +1,42 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ # A factory that builds a batch connect template object from a
6
+ # configuration.
7
+ class Factory
8
+ using Refinements::HashExtensions
9
+
10
+ class << self
11
+ # Build a batch connect template from a configuration
12
+ # @param config [#to_h] configuration describing batch connect template
13
+ # @option config [#to_s] :template The batch connect template to use
14
+ # @raise [TemplateNotSpecified] if no template is specified
15
+ # @raise [TemplateNotFound] if the specified template does not exist
16
+ # @return [Template] the batch connect template object
17
+ def build(config)
18
+ c = config.to_h.symbolize_keys
19
+
20
+ template = c.fetch(:template) { raise TemplateNotSpecified, "batch connect configuration does not specify template" }.to_s
21
+
22
+ path_to_template = "ood_core/batch_connect/templates/#{template}"
23
+ begin
24
+ require path_to_template
25
+ rescue Gem::LoadError => e
26
+ raise Gem::LoadError, "Specified '#{template}' for batch connect template, but the gem is not loaded."
27
+ rescue LoadError => e
28
+ raise LoadError, "Could not load '#{template}'. Make sure that that batch connect template in the configuration file is valid."
29
+ end
30
+
31
+ template_method = "build_#{template}"
32
+
33
+ unless respond_to?(template_method)
34
+ raise TemplateNotFound, "batch connect configuration specifies nonexistent #{template} template"
35
+ end
36
+
37
+ send(template_method, c)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,207 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ # A template class that renders a batch script designed to facilitate
6
+ # external connections to the running job
7
+ class Template
8
+ using Refinements::HashExtensions
9
+ using Refinements::ArrayExtensions
10
+
11
+ # The context used to render this template
12
+ # @return [Hash] context hash
13
+ attr_reader :context
14
+
15
+ # @param context [#to_h] the context used to render the template
16
+ # @option context [#to_s] :work_dir Working directory for batch script
17
+ # @option context [#to_s] :conn_file ("connection.yml") The file that
18
+ # holds connection information
19
+ # @option context [#to_sym, Array<#to_sym>] :conn_params ([]) A list of
20
+ # connection parameters added to the connection file (`:host`, `:port`,
21
+ # and `:password` will always exist)
22
+ # @option context [#to_s] :bash_helpers ("...") Bash helper methods
23
+ # @option context [#to_i] :min_port (2000) Minimum port used when looking
24
+ # for available port
25
+ # @option context [#to_i] :max_port (65535) Maximum port used when
26
+ # looking for available port
27
+ # @option context [#to_i] :passwd_size (32) Length of randomly generated
28
+ # password
29
+ # @option context [#to_s] :script_wrapper ("%s") Bash code that wraps
30
+ # around the body of the template script (use `%s` to interpolate the
31
+ # body)
32
+ # @option context [#to_s] :before_script ("...") Bash code run before the
33
+ # main script is forked off
34
+ # @option context [#to_s] :before_file ("before.sh") Path to script that
35
+ # is sourced before main script is forked (assumes you don't modify
36
+ # `:before_script`)
37
+ # @option context [#to_s] :run_script ("...") Bash code that is forked
38
+ # off and treated as the main script
39
+ # @option context [#to_s] :script_file ("./script.sh") Path to script
40
+ # that is forked as the main scripta (assumes you don't modify
41
+ # `:run_script`)
42
+ # @option context [#to_s] :timeout ("") Timeout the main script in
43
+ # seconds, if empty then let script run for full walltime (assumes you
44
+ # don't modify `:run_script`)
45
+ # @option context [#to_s] :clean_script ("...") Bash code run during
46
+ # clean up after job finishes
47
+ # @option context [#to_s] :clean_file ("clean.sh") Path to script that is
48
+ # sourced during clean up (assumes you don't modify `:clean_script`)
49
+ def initialize(context = {})
50
+ @context = context.to_h.compact.symbolize_keys
51
+ raise ArgumentError, "No work_dir specified. Missing argument: work_dir" unless context.include?(:work_dir)
52
+ end
53
+
54
+ # Render this template as string
55
+ # @return [String] rendered template
56
+ def to_s
57
+ <<-EOT.gsub(/^ {10}/, '')
58
+ #!/bin/bash
59
+
60
+ #{script_wrapper}
61
+ EOT
62
+ end
63
+
64
+ private
65
+ # Working directory that batch script runs in
66
+ def work_dir
67
+ context.fetch(:work_dir).to_s
68
+ end
69
+
70
+ # The file that holds the connection information in yaml format
71
+ def conn_file
72
+ context.fetch(:conn_file, "connection.yml").to_s
73
+ end
74
+
75
+ # The parameters to include in the connection file
76
+ def conn_params
77
+ conn_params = Array.wrap(context.fetch(:conn_params, [])).map(&:to_sym)
78
+ (conn_params + [:host, :port, :password]).uniq
79
+ end
80
+
81
+ # Helper methods used in the bash scripts
82
+ def bash_helpers
83
+ context.fetch(:bash_helpers) do
84
+ min_port = context.fetch(:min_port, 2000).to_i
85
+ max_port = context.fetch(:max_port, 65535).to_i
86
+ passwd_size = context.fetch(:passwd_size, 32).to_i
87
+
88
+ <<-EOT.gsub(/^ {14}/, '')
89
+ # Generate random integer in range [$1..$2]
90
+ function random () {
91
+ shuf -i ${1}-${2} -n 1
92
+ }
93
+
94
+ # Check if port $1 is in use
95
+ function used_port () {
96
+ local PORT=${1}
97
+ nc -z localhost ${PORT} &>/dev/null
98
+ }
99
+
100
+ # Find available port in range [$1..$2]
101
+ # Default: [#{min_port}..#{max_port}]
102
+ function find_port () {
103
+ local PORT=$(random ${1:-#{min_port}} ${2:-#{max_port}})
104
+ while $(used_port ${PORT}); do
105
+ PORT=$(random ${1:-#{min_port}} ${2:-#{max_port}})
106
+ done
107
+ echo ${PORT}
108
+ }
109
+
110
+ # Generate random alphanumeric password with $1 (default: #{passwd_size}) characters
111
+ function create_passwd () {
112
+ tr -cd '[:alnum:]' < /dev/urandom 2>/dev/null | head -c${1:-#{passwd_size}}
113
+ }
114
+ EOT
115
+ end.to_s
116
+ end
117
+
118
+ # Bash code that wraps around the body of the template script (use `%s`
119
+ # to interpolate the body)
120
+ def script_wrapper
121
+ context.fetch(:script_wrapper, "%s").to_s % base_script
122
+ end
123
+
124
+ # Source in a developer defined script before running the main script
125
+ def before_script
126
+ context.fetch(:before_script) do
127
+ before_file = context.fetch(:before_file, "before.sh").to_s
128
+
129
+ "host=$(hostname)\n[[ -e \"#{before_file}\" ]] && source \"#{before_file}\""
130
+ end.to_s
131
+ end
132
+
133
+ # Fork off a developer defined main script and possibly time it out after
134
+ # a period of time
135
+ def run_script
136
+ context.fetch(:run_script) do
137
+ script_file = context.fetch(:script_file, "./script.sh").to_s
138
+ timeout = context.fetch(:timeout, "").to_s
139
+
140
+ timeout.empty? ? "\"#{script_file}\"" : "timeout #{timeout} \"#{script_file}\""
141
+ end.to_s
142
+ end
143
+
144
+ # Source in a developer defined script after running the main script
145
+ def after_script
146
+ context.fetch(:after_script) do
147
+ after_file = context.fetch(:after_file, "after.sh").to_s
148
+
149
+ "[[ -e \"#{after_file}\" ]] && source \"#{after_file}\""
150
+ end.to_s
151
+ end
152
+
153
+ # Source in a developer defined clean up script that is run during the
154
+ # clean up stage
155
+ def clean_script
156
+ context.fetch(:clean_script) do
157
+ clean_file = context.fetch(:clean_file, "clean.sh").to_s
158
+
159
+ "[[ -e \"#{clean_file}\" ]] && source \"#{clean_file}\""
160
+ end.to_s
161
+ end
162
+
163
+ # The base script template
164
+ def base_script
165
+ <<-EOT.gsub(/^ {12}/, '')
166
+ cd #{work_dir}
167
+
168
+ # Generate a connection yaml file with given parameters
169
+ function create_yml () {
170
+ echo "Generating connection YAML file..."
171
+ (
172
+ umask 077
173
+ echo -e "#{conn_params.map { |p| "#{p}: $#{p}" }.join('\n')}" > "#{conn_file}"
174
+ )
175
+ }
176
+
177
+ # Cleanliness is next to Godliness
178
+ function clean_up () {
179
+ echo "Cleaning up..."
180
+ #{clean_script.gsub(/\n(?=[^\s])/, "\n ")}
181
+ pkill -P $$
182
+ exit ${1:-0}
183
+ }
184
+
185
+ #{bash_helpers}
186
+
187
+ #{before_script}
188
+
189
+ echo "Script starting..."
190
+ #{run_script} &
191
+ SCRIPT_PID=$!
192
+
193
+ #{after_script}
194
+
195
+ # Create the connection yaml file
196
+ create_yml
197
+
198
+ # Wait for script process to finish
199
+ wait ${SCRIPT_PID} || clean_up 1
200
+
201
+ # Exit cleanly
202
+ clean_up
203
+ EOT
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,23 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ class Factory
6
+ using Refinements::HashExtensions
7
+
8
+ # Build the basic template from a configuration
9
+ # @param config [#to_h] the configuration for the batch connect template
10
+ def self.build_basic(config)
11
+ context = config.to_h.symbolize_keys.reject { |k, _| k == :template }
12
+ Templates::Basic.new(context)
13
+ end
14
+ end
15
+
16
+ module Templates
17
+ # A batch connect template that expects to start up a basic web server
18
+ # within a batch job
19
+ class Basic < Template
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,201 @@
1
+ require "ood_core/refinements/hash_extensions"
2
+
3
+ module OodCore
4
+ module BatchConnect
5
+ class Factory
6
+ using Refinements::HashExtensions
7
+
8
+ # Build the VNC template from a configuration
9
+ # @param config [#to_h] the configuration for the batch connect template
10
+ def self.build_vnc(config)
11
+ context = config.to_h.symbolize_keys.reject { |k, _| k == :template }
12
+ Templates::VNC.new(context)
13
+ end
14
+ end
15
+
16
+ module Templates
17
+ # A batch connect template that starts up a VNC server within a batch job
18
+ class VNC < Template
19
+ # @param context [#to_h] the context used to render the template
20
+ # @option context [#to_sym, Array<#to_sym>] :conn_params ([]) A list of
21
+ # connection parameters added to the connection file (`:host`,
22
+ # `:port`, `:password`, `:spassword`, `:display` and `:websocket`
23
+ # will always exist)
24
+ # @option context [#to_s] :websockify_cmd
25
+ # ("${WEBSOCKIFY_CMD:-/opt/websockify/run}") the path to the
26
+ # websockify script (assumes you don't modify `:after_script`)
27
+ # @option context [#to_s] :vnc_log ("vnc.log") path to vnc server log
28
+ # file (assumes you don't modify `:before_script` or `:after_script`)
29
+ # @option context [#to_s] :vnc_passwd ("vnc.passwd") path to the file
30
+ # generated that contains the encrypted vnc password (assumes you
31
+ # don't modify `:before_script`)
32
+ # @option context [#to_s] :vnc_args arguments used when starting up the
33
+ # vnc server (overrides any specific vnc argument) (assumes you don't
34
+ # modify `:before_script`)
35
+ # @option context [#to_s] :name ("") name of the vnc server session
36
+ # (not set if blank or `:vnc_args` is set) (assumes you don't modify
37
+ # `:before_script`)
38
+ # @option context [#to_s] :geometry ("") resolution of vnc display (not
39
+ # set if blank or `:vnc_args` is set) (assumes you don't modify
40
+ # `:before_script`)
41
+ # @option context [#to_s] :dpi ("") dpi of vnc display (not set if
42
+ # blank or `:vnc_args` is set) (assumes you don't modify
43
+ # `:before_script`)
44
+ # @option context [#to_s] :fonts ("") command delimited list of fonts
45
+ # available in vnc display (not set if blank or `:vnc_args` is set)
46
+ # (assumes you don't modify `:before_script`)
47
+ # @option context [#to_s] :idle ("") timeout vnc server if no
48
+ # connection in this amount of time in seconds (not set if blank or
49
+ # `:vnc_args` is set) (assumes you don't modify `:before_script`)
50
+ # @option context [#to_s] :extra_args ("") any extra arguments used
51
+ # when initializing the vnc server process (not set if blank or
52
+ # `:vnc_args` is set) (assumes you don't modify `:before_script`)
53
+ # @option context [#to_s] :vnc_clean ("...") script used to clean up
54
+ # any active vnc sessions (assumes you don't modify `:before_script`
55
+ # or `:clean_script`)
56
+ # @see Template
57
+ def initialize(context = {})
58
+ super
59
+ end
60
+
61
+ private
62
+ # We need to know the VNC and websockify connection information
63
+ def conn_params
64
+ (super + [:display, :websocket, :spassword]).uniq
65
+ end
66
+
67
+ # Before running the main script, start up a VNC server and record
68
+ # the connection information
69
+ def before_script
70
+ <<-EOT.gsub(/^ {14}/, "")
71
+ # Setup one-time use passwords and initialize the VNC password
72
+ function change_passwd () {
73
+ echo "Setting VNC password..."
74
+ password=$(create_passwd 8)
75
+ spassword=${spassword:-$(create_passwd 8)}
76
+ (
77
+ umask 077
78
+ echo -ne "${password}\\n${spassword}" | vncpasswd -f > "#{vnc_passwd}"
79
+ )
80
+ }
81
+ change_passwd
82
+
83
+ # Start up vnc server (if at first you don't succeed, try, try again)
84
+ echo "Starting VNC server..."
85
+ for i in $(seq 1 10); do
86
+ # Clean up any old VNC sessions that weren't cleaned before
87
+ #{vnc_clean}
88
+
89
+ # Attempt to start VNC server
90
+ VNC_OUT=$(vncserver -log "#{vnc_log}" -rfbauth "#{vnc_passwd}" -nohttpd -noxstartup #{vnc_args} 2>&1)
91
+ VNC_PID=$(pgrep -s 0 Xvnc) # the script above will daemonize the Xvnc process
92
+ echo "${VNC_OUT}"
93
+
94
+ # Sometimes Xvnc hangs if it fails to find working disaply, we
95
+ # should kill it and try again
96
+ kill -0 ${VNC_PID} 2>/dev/null && [[ "${VNC_OUT}" =~ "Fatal server error" ]] && kill -TERM ${VNC_PID}
97
+
98
+ # Check that Xvnc process is running, if not assume it died and
99
+ # wait some random period of time before restarting
100
+ kill -0 ${VNC_PID} 2>/dev/null || sleep 0.$(random 1 9)s
101
+
102
+ # If running, then all is well and break out of loop
103
+ kill -0 ${VNC_PID} 2>/dev/null && break
104
+ done
105
+
106
+ # If we fail to start it after so many tries, then just give up
107
+ kill -0 ${VNC_PID} 2>/dev/null || clean_up 1
108
+
109
+ # Parse output for ports used
110
+ display=$(echo "${VNC_OUT}" | awk -F':' '/^Desktop/{print $NF}')
111
+ port=$((5900+display))
112
+
113
+ echo "Successfully started VNC server on ${host}:${port}..."
114
+
115
+ #{super}
116
+ EOT
117
+ end
118
+
119
+ # Run the script under the VNC server's display
120
+ def run_script
121
+ %(DISPLAY=:${display} #{super})
122
+ end
123
+
124
+ # After startup the main script, scan the VNC server log file for
125
+ # successful connections so that the password can be reset
126
+ def after_script
127
+ websockify_cmd = context.fetch(:websockify_cmd, "${WEBSOCKIFY_CMD:-/opt/websockify/run}").to_s
128
+
129
+ <<-EOT.gsub(/^ {14}/, "")
130
+ #{super}
131
+
132
+ # Launch websockify websocket server
133
+ echo "Starting websocket server..."
134
+ websocket=$(find_port)
135
+ #{websockify_cmd} -D ${websocket} localhost:${port}
136
+
137
+ # Set up background process that scans the log file for successful
138
+ # connections by users, and change the password after every
139
+ # connection
140
+ echo "Scanning VNC log file for user authentications..."
141
+ while read -r line; do
142
+ if [[ ${line} =~ "Full-control authentication enabled for" ]]; then
143
+ change_passwd
144
+ create_yml
145
+ fi
146
+ done < <(tail -f --pid=${SCRIPT_PID} "#{vnc_log}") &
147
+ EOT
148
+ end
149
+
150
+ # Clean up the running VNC server and any other stale VNC servers
151
+ def clean_script
152
+ <<-EOT.gsub(/^ {14}/, "")
153
+ #{super}
154
+
155
+ #{vnc_clean}
156
+ [[ -n ${display} ]] && vncserver -kill :${display}
157
+ EOT
158
+ end
159
+
160
+ # Log file for VNC server
161
+ def vnc_log
162
+ context.fetch(:vnc_log, "vnc.log").to_s
163
+ end
164
+
165
+ # Password file for VNC server
166
+ def vnc_passwd
167
+ context.fetch(:vnc_passwd, "vnc.passwd").to_s
168
+ end
169
+
170
+ # Arguments sent to `vncserver` command
171
+ def vnc_args
172
+ context.fetch(:vnc_args) do
173
+ name = context.fetch(:name, "").to_s
174
+ geometry = context.fetch(:geometry, "").to_s
175
+ dpi = context.fetch(:dpi, "").to_s
176
+ fonts = context.fetch(:fonts, "").to_s
177
+ idle = context.fetch(:idle, "").to_s
178
+ extra_args = context.fetch(:extra_args, "").to_s
179
+
180
+ args = []
181
+ args << "-name #{name}" unless name.empty?
182
+ args << "-geometry #{geometry}" unless geometry.empty?
183
+ args << "-dpi #{dpi}" unless dpi.empty?
184
+ args << "-fp #{fonts}" unless fonts.empty?
185
+ args << "-idletimeout #{idle}" unless idle.empty?
186
+ args << extra_args
187
+
188
+ args.join(" ")
189
+ end.to_s
190
+ end
191
+
192
+ # Clean up any stale VNC sessions
193
+ def vnc_clean
194
+ context.fetch(:vnc_clean) do
195
+ %(vncserver -list | awk '/^:/{system("kill -0 "$2" 2>/dev/null || vncserver -kill "$1)}')
196
+ end.to_s
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end