vcap_common 1.0.10

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.
@@ -0,0 +1,236 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require 'fileutils'
3
+ require 'socket'
4
+
5
+ # VMware's Cloud Application Platform
6
+
7
+ module VCAP
8
+
9
+ A_ROOT_SERVER = '198.41.0.4'
10
+
11
+ def self.local_ip(route = A_ROOT_SERVER)
12
+ route ||= A_ROOT_SERVER
13
+ orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
14
+ UDPSocket.open {|s| s.connect(route, 1); s.addr.last }
15
+ ensure
16
+ Socket.do_not_reverse_lookup = orig
17
+ end
18
+
19
+ def self.secure_uuid
20
+ result = File.open('/dev/urandom') { |x| x.read(16).unpack('H*')[0] }
21
+ end
22
+
23
+ def self.grab_ephemeral_port
24
+ socket = TCPServer.new('0.0.0.0', 0)
25
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
26
+ Socket.do_not_reverse_lookup = true
27
+ port = socket.addr[1]
28
+ socket.close
29
+ return port
30
+ end
31
+
32
+ def self.uptime_string(delta)
33
+ num_seconds = delta.to_i
34
+ days = num_seconds / (60 * 60 * 24);
35
+ num_seconds -= days * (60 * 60 * 24);
36
+ hours = num_seconds / (60 * 60);
37
+ num_seconds -= hours * (60 * 60);
38
+ minutes = num_seconds / 60;
39
+ num_seconds -= minutes * 60;
40
+ "#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s"
41
+ end
42
+
43
+ def self.num_cores
44
+ if RUBY_PLATFORM =~ /linux/
45
+ return `cat /proc/cpuinfo | grep processor | wc -l`.to_i
46
+ elsif RUBY_PLATFORM =~ /darwin/
47
+ `hwprefs cpu_count`.strip.to_i
48
+ elsif RUBY_PLATFORM =~ /freebsd|netbsd/
49
+ `sysctl hw.ncpu`.strip.to_i
50
+ else
51
+ return 1 # unknown..
52
+ end
53
+ rescue
54
+ # hwprefs doesn't always exist, and so the block above can fail.
55
+ # In any case, let's always assume that there is 1 core
56
+ 1
57
+ end
58
+
59
+ def self.defer(*args, &blk)
60
+ if args[0].kind_of?(Hash)
61
+ op = blk
62
+ opts = args[0]
63
+ else
64
+ op = args[0] || blk
65
+ opts = args[1] || {}
66
+ end
67
+
68
+ callback = opts[:callback]
69
+ logger = opts[:logger]
70
+ nobacktrace = opts[:nobacktrace]
71
+
72
+ wrapped_operation = exception_wrap_block(op, logger, nobacktrace)
73
+ wrapped_callback = callback ? exception_wrap_block(callback, logger, nobacktrace) : nil
74
+ EM.defer(wrapped_operation, wrapped_callback)
75
+ end
76
+
77
+ def self.exception_wrap_block(op, logger, nobacktrace=false)
78
+ Proc.new do |*args|
79
+ begin
80
+ op.call(*args)
81
+ rescue => e
82
+ err_str = "#{e} - #{e.backtrace.join("\n")}" unless nobacktrace
83
+ err_str = "#{e}" if nobacktrace
84
+ if logger
85
+ logger.fatal(err_str)
86
+ else
87
+ $stderr.puts(err_str)
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def self.process_running?(pid)
94
+ return false unless pid && (pid > 0)
95
+ output = %x[ps -o rss= -p #{pid}]
96
+ return true if ($? == 0 && !output.empty?)
97
+ # fail otherwise..
98
+ return false
99
+ end
100
+
101
+ def self.pp_bytesize(bsize)
102
+ units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
103
+ base = 1000
104
+ bsize = bsize.to_f()
105
+ quotient = unit = nil
106
+ units.each_with_index do |u, i|
107
+ unit = u
108
+ quotient = bsize / (base ** i)
109
+ break if quotient < base
110
+ end
111
+ "%0.2f%s" % [quotient, unit]
112
+ end
113
+
114
+ def self.symbolize_keys(hash)
115
+ if hash.is_a? Hash
116
+ new_hash = {}
117
+ hash.each {|k, v| new_hash[k.to_sym] = symbolize_keys(v) }
118
+ new_hash
119
+ else
120
+ hash
121
+ end
122
+ end
123
+
124
+ # Helper class to atomically create/update pidfiles and ensure that only one instance of a given
125
+ # process is running at all times.
126
+ #
127
+ # NB: Ruby doesn't have real destructors so if you want to be polite and clean up after yourself
128
+ # be sure to call unlink() before your process exits.
129
+ #
130
+ # usage:
131
+ #
132
+ # begin
133
+ # pidfile = VCAP::PidFile.new('/tmp/foo')
134
+ # rescue => e
135
+ # puts "Error creating pidfile: %s" % (e)
136
+ # exit(1)
137
+ # end
138
+ class PidFile
139
+ class ProcessRunningError < StandardError
140
+ end
141
+
142
+ def initialize(pid_file, create_parents=true)
143
+ @pid_file = pid_file
144
+ @dirty = true
145
+ write(create_parents)
146
+ end
147
+
148
+ # Removes the created pidfile
149
+ def unlink()
150
+ return unless @dirty
151
+
152
+ # Swallowing exception here is fine. Removing the pid files is a courtesy.
153
+ begin
154
+ File.unlink(@pid_file)
155
+ @dirty = false
156
+ rescue
157
+ end
158
+ self
159
+ end
160
+
161
+ # Removes the created pidfile upon receipt of the supplied signals
162
+ def unlink_on_signals(*sigs)
163
+ return unless @dirty
164
+
165
+ sigs.each do |s|
166
+ Signal.trap(s) { unlink() }
167
+ end
168
+ self
169
+ end
170
+
171
+ def unlink_at_exit()
172
+ at_exit { unlink() }
173
+ self
174
+ end
175
+
176
+ def to_s()
177
+ @pid_file
178
+ end
179
+
180
+ protected
181
+
182
+ # Atomically writes the pidfile.
183
+ # NB: This throws exceptions if the pidfile contains the pid of another running process.
184
+ #
185
+ # +create_parents+ If true, all parts of the path up to the file's dirname will be created.
186
+ #
187
+ def write(create_parents=true)
188
+ FileUtils.mkdir_p(File.dirname(@pid_file)) if create_parents
189
+
190
+ # Protip from Wilson: binary mode keeps things sane under Windows
191
+ # Closing the fd releases our lock
192
+ File.open(@pid_file, 'a+b', 0644) do |f|
193
+ f.flock(File::LOCK_EX)
194
+
195
+ # Check if process is already running
196
+ pid = f.read().strip().to_i()
197
+ if pid == Process.pid()
198
+ return
199
+ elsif VCAP.process_running?(pid)
200
+ raise ProcessRunningError.new("Process already running (pid=%d)." % (pid))
201
+ end
202
+
203
+ # We're good to go, write our pid
204
+ f.truncate(0)
205
+ f.rewind()
206
+ f.write("%d\n" % (Process.pid()))
207
+ f.flush()
208
+ end
209
+ end
210
+ end # class PidFile
211
+
212
+ end # module VCAP
213
+
214
+ # Make the patch here for proper bytesize
215
+ if RUBY_VERSION <= "1.8.6"
216
+ class String #:nodoc:
217
+ def bytesize; self.size; end
218
+ end
219
+ end
220
+
221
+ # FIXME, we should ditch ruby logger.
222
+ # Monkey Patch to get rid of some deadlocks under load in CC, make it available for all.
223
+
224
+ require 'logger'
225
+
226
+ STDOUT.sync = true
227
+
228
+ class Logger::LogDevice
229
+ def write(message)
230
+ @dev.syswrite(message)
231
+ end
232
+
233
+ def close
234
+ @dev.close
235
+ end
236
+ end
@@ -0,0 +1,172 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require "eventmachine"
3
+ require 'thin'
4
+ require "yajl"
5
+ require "nats/client"
6
+ require "base64"
7
+ require 'set'
8
+
9
+ module VCAP
10
+
11
+ RACK_JSON_HDR = { 'Content-Type' => 'application/json' }
12
+ RACK_TEXT_HDR = { 'Content-Type' => 'text/plaintext' }
13
+
14
+ class Varz
15
+ def initialize(logger)
16
+ @logger = logger
17
+ end
18
+
19
+ def call(env)
20
+ @logger.debug "varz access"
21
+ varz = Yajl::Encoder.encode(Component.updated_varz, :pretty => true, :terminator => "\n")
22
+ [200, { 'Content-Type' => 'application/json', 'Content-Length' => varz.length.to_s }, varz]
23
+ rescue => e
24
+ @logger.error "varz error #{e.inspect} #{e.backtrace.join("\n")}"
25
+ raise e
26
+ end
27
+ end
28
+
29
+ class Healthz
30
+ def initialize(logger)
31
+ @logger = logger
32
+ end
33
+
34
+ def call(env)
35
+ @logger.debug "healthz access"
36
+ healthz = Component.updated_healthz
37
+ [200, { 'Content-Type' => 'application/json', 'Content-Length' => healthz.length.to_s }, healthz]
38
+ rescue => e
39
+ @logger.error "healthz error #{e.inspect} #{e.backtrace.join("\n")}"
40
+ raise e
41
+ end
42
+ end
43
+
44
+ # Common component setup for discovery and monitoring
45
+ class Component
46
+
47
+ # We will suppress these from normal varz reporting by default.
48
+ CONFIG_SUPPRESS = Set.new([:mbus, :service_mbus, :keys, :database_environment, :mysql, :password])
49
+
50
+ class << self
51
+
52
+ attr_reader :varz
53
+ attr_accessor :healthz
54
+
55
+ def updated_varz
56
+ @last_varz_update ||= 0
57
+ if Time.now.to_f - @last_varz_update >= 1
58
+ # Snapshot uptime
59
+ @varz[:uptime] = VCAP.uptime_string(Time.now - @varz[:start])
60
+
61
+ # Grab current cpu and memory usage.
62
+ rss, pcpu = `ps -o rss=,pcpu= -p #{Process.pid}`.split
63
+ @varz[:mem] = rss.to_i
64
+ @varz[:cpu] = pcpu.to_f
65
+
66
+ @last_varz_update = Time.now.to_f
67
+ end
68
+ varz
69
+ end
70
+
71
+ def updated_healthz
72
+ @last_healthz_update ||= 0
73
+ if Time.now.to_f - @last_healthz_update >= 1
74
+ # ...
75
+ @last_healthz_update = Time.now.to_f
76
+ end
77
+
78
+ healthz
79
+ end
80
+
81
+ def start_http_server(host, port, auth, logger)
82
+ http_server = Thin::Server.new(host, port, :signals => false) do
83
+ Thin::Logging.silent = true
84
+ use Rack::Auth::Basic do |username, password|
85
+ [username, password] == auth
86
+ end
87
+ map '/healthz' do
88
+ run Healthz.new(logger)
89
+ end
90
+ map '/varz' do
91
+ run Varz.new(logger)
92
+ end
93
+ end
94
+ http_server.start!
95
+ end
96
+
97
+ def uuid
98
+ @discover[:uuid]
99
+ end
100
+
101
+ def register(opts)
102
+ uuid = VCAP.secure_uuid
103
+ type = opts[:type]
104
+ index = opts[:index]
105
+ uuid = "#{index}-#{uuid}" if index
106
+ host = opts[:host] || VCAP.local_ip
107
+ port = opts[:port] || VCAP.grab_ephemeral_port
108
+ nats = opts[:nats] || NATS
109
+ auth = [opts[:user] || VCAP.secure_uuid, opts[:password] || VCAP.secure_uuid]
110
+ logger = opts[:logger] || Logger.new(nil)
111
+
112
+ # Discover message limited
113
+ @discover = {
114
+ :type => type,
115
+ :index => index,
116
+ :uuid => uuid,
117
+ :host => "#{host}:#{port}",
118
+ :credentials => auth,
119
+ :start => Time.now
120
+ }
121
+
122
+ # Varz is customizable
123
+ @varz = @discover.dup
124
+ @varz[:num_cores] = VCAP.num_cores
125
+ @varz[:config] = sanitize_config(opts[:config]) if opts[:config]
126
+
127
+ @healthz = "ok\n".freeze
128
+
129
+ # Next steps require EM
130
+ raise "EventMachine reactor needs to be running" if !EventMachine.reactor_running?
131
+
132
+ # Startup the http endpoint for /varz and /healthz
133
+ start_http_server(host, port, auth, logger)
134
+
135
+ # Listen for discovery requests
136
+ nats.subscribe('vcap.component.discover') do |msg, reply|
137
+ update_discover_uptime
138
+ nats.publish(reply, @discover.to_json)
139
+ end
140
+
141
+ # Also announce ourselves on startup..
142
+ nats.publish('vcap.component.announce', @discover.to_json)
143
+ end
144
+
145
+ def update_discover_uptime
146
+ @discover[:uptime] = VCAP.uptime_string(Time.now - @discover[:start])
147
+ end
148
+
149
+ def clear_level(h)
150
+ h.each do |k, v|
151
+ if CONFIG_SUPPRESS.include?(k.to_sym)
152
+ h.delete(k)
153
+ else
154
+ clear_level(h[k]) if v.instance_of? Hash
155
+ end
156
+ end
157
+ end
158
+
159
+ def sanitize_config(config)
160
+ # Can't Marshal/Deep Copy logger instances that services use
161
+ if config[:logger]
162
+ config = config.dup
163
+ config.delete(:logger)
164
+ end
165
+ # Deep copy
166
+ config = Marshal.load(Marshal.dump(config))
167
+ clear_level(config)
168
+ config
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,32 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require 'yaml'
3
+
4
+ require 'vcap/common'
5
+ require 'vcap/json_schema'
6
+
7
+ module VCAP
8
+ class Config
9
+ class << self
10
+ attr_reader :schema
11
+
12
+ def define_schema(&blk)
13
+ @schema = VCAP::JsonSchema.build(&blk)
14
+ end
15
+
16
+ def from_file(filename, symbolize_keys=true)
17
+ config = YAML.load_file(filename)
18
+ @schema.validate(config)
19
+ config = VCAP.symbolize_keys(config) if symbolize_keys
20
+ config
21
+ end
22
+
23
+ def to_file(config, out_filename)
24
+ @schema.validate(config)
25
+ File.open(out_filename, 'w+') do |f|
26
+ YAML.dump(config, f)
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ class Fiber
3
+ attr_accessor :trace_id
4
+ alias_method :orig_resume, :resume
5
+
6
+ class << self
7
+ @io=nil
8
+
9
+ alias_method :orig_yield, :yield
10
+
11
+ def enable_tracing(io)
12
+ raise ArgumentError, "You must pass in IO object, #{io.class} given" unless io.is_a? IO
13
+ @io = io
14
+ end
15
+
16
+ def yield(*args)
17
+ log_action('yield')
18
+ begin
19
+ orig_yield(*args)
20
+ rescue FiberError => fe
21
+ Fiber.log_action('yield_error', self)
22
+ raise fe
23
+ end
24
+ end
25
+
26
+ def log_action(action, f=nil)
27
+ return unless @io
28
+ f ||= Fiber.current
29
+ trace_id = f.trace_id || '-'
30
+ cname = Kernel.caller[1]
31
+ @io.puts("FT %-14s %-20s %-30s %s" % [action, trace_id, f.object_id, cname])
32
+ @io.flush
33
+ end
34
+ end
35
+
36
+ def resume(*args)
37
+ Fiber.log_action('resume', self)
38
+ begin
39
+ orig_resume(*args)
40
+ rescue FiberError => fe
41
+ Fiber.log_action('resume_error', self)
42
+ raise fe
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,202 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+
3
+ # This provides very basic support for creating declarative validators
4
+ # for decoded json. Useful for validating things like configs or
5
+ # messages that are published via NATS.
6
+ #
7
+ # Basic usage:
8
+ #
9
+ # schema = VCAP::JsonSchema.build do
10
+ # { :foo => [String],
11
+ # :bar => {:baz => Integer},
12
+ # optional(:jaz) => Float,
13
+ # }
14
+ # end
15
+ #
16
+ # Fails:
17
+ #
18
+ # schema.validate({
19
+ # 'foo' => ['bar', 5],
20
+ # 'bar' => {'baz' => 7},
21
+ # })
22
+ #
23
+ # Succeeds:
24
+ #
25
+ # schema.validate({
26
+ # 'foo' => ['bar', 'baz'],
27
+ # 'bar' => {'baz' => 7},
28
+ # })
29
+ #
30
+ #
31
+ module VCAP
32
+ module JsonSchema
33
+
34
+ class SyntaxError < StandardError; end
35
+ class ValidationError < StandardError
36
+ attr_accessor :message
37
+ def initialize(msg)
38
+ @message = msg
39
+ end
40
+
41
+ def to_s
42
+ @message
43
+ end
44
+ end
45
+
46
+ class ValueError < ValidationError; end
47
+ class TypeError < ValidationError; end
48
+ class MissingKeyError < ValidationError; end
49
+
50
+ # Defines an interface that all schemas must implement
51
+ class BaseSchema
52
+ # Verfies that val conforms to the schema being validated
53
+ # Throws exceptions derived from ValidationError upon schema violations
54
+ #
55
+ # @param Object val An object decoded from json
56
+ #
57
+ # @return nil
58
+ def validate(dec_json)
59
+ raise NotImplementedError.new("You must implement validate")
60
+ end
61
+ end
62
+
63
+ class BoolSchema < BaseSchema
64
+ def validate(dec_json)
65
+ unless dec_json.kind_of?(TrueClass) || dec_json.kind_of?(FalseClass)
66
+ raise TypeError, "Expected instance of TrueClass or FalseClass, got #{dec_json.class}"
67
+ end
68
+ end
69
+ end
70
+
71
+ # Checks that supplied value is an instance of a given class
72
+ class TypeSchema < BaseSchema
73
+ def initialize(klass)
74
+ raise ArgumentError, "You must supply a class #{klass} given" unless klass.kind_of?(Class)
75
+ @klass = klass
76
+ end
77
+
78
+ def validate(dec_json)
79
+ raise TypeError, "Expected instance of #{@klass}, got #{dec_json.class}" unless dec_json.kind_of?(@klass)
80
+ end
81
+ end
82
+
83
+ # Checks that supplied value is an array, and each value in the array validates
84
+ class ArraySchema < BaseSchema
85
+ attr_accessor :item_schema
86
+
87
+ def initialize(schema)
88
+ raise ArgumentError, "You must supply a schema, #{schema.class} given" unless schema.kind_of?(BaseSchema)
89
+ @item_schema = schema
90
+ end
91
+
92
+ def validate(dec_json)
93
+ raise TypeError, "Expected instance of Array, #{dec_json.class} given" unless dec_json.kind_of?(Array)
94
+ for v in dec_json
95
+ @item_schema.validate(v)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Check that required keys are present, and that they validate
101
+ class HashSchema < BaseSchema
102
+ class Field
103
+ attr_reader :name, :schema
104
+ attr_accessor :optional
105
+
106
+ def initialize(name, schema, optional=false)
107
+ raise ArgumentError, "Schema must be an instance of a schema, #{schema.class} given #{schema.inspect}" unless schema.kind_of?(BaseSchema)
108
+ @name = name
109
+ @schema = schema
110
+ @optional = optional
111
+ end
112
+ end
113
+
114
+ def initialize(kvs={})
115
+ raise ArgumentError, "Expected Hash, #{kvs.class} given" unless kvs.is_a?(Hash)
116
+ # Convert symbols to strings. Validation will be performed against decoded json, which will have
117
+ # string keys.
118
+ @fields = {}
119
+ for k, v in kvs
120
+ raise ArgumentError, "Expected schema for key #{k}, got #{v.class}" unless v.kind_of?(BaseSchema)
121
+ k_s = k.to_s
122
+ @fields[k_s] = Field.new(k_s, v, false)
123
+ end
124
+ end
125
+
126
+ def required(name, schema)
127
+ name_s = name.to_s
128
+ @fields[name_s] = Field.new(name_s, schema, false)
129
+ end
130
+
131
+ def optional(name, schema)
132
+ name_s = name.to_s
133
+ @fields[name_s] = Field.new(name_s, schema, true)
134
+ end
135
+
136
+ def validate(dec_json)
137
+ raise TypeError, "Expected instance of Hash, got instance of #{dec_json.class}" unless dec_json.kind_of?(Hash)
138
+
139
+ missing_keys = []
140
+ for k in @fields.keys
141
+ missing_keys << k unless dec_json.has_key?(k) || @fields[k].optional
142
+ end
143
+ raise MissingKeyError, "Missing keys: #{missing_keys.join(', ')}" unless missing_keys.empty?
144
+
145
+ for k, f in @fields
146
+ next if f.optional && !dec_json.has_key?(k)
147
+ begin
148
+ f.schema.validate(dec_json[k])
149
+ rescue ValidationError => ve
150
+ ve.message = "'#{k}' => " + ve.message
151
+ raise ve
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ class << self
158
+ class OptionalKeyMarker
159
+ attr_reader :name
160
+ def initialize(name)
161
+ @name = name
162
+ end
163
+
164
+ def to_s
165
+ @name.to_s
166
+ end
167
+ end
168
+
169
+ def optional(key)
170
+ OptionalKeyMarker.new(key)
171
+ end
172
+
173
+ def build(&blk)
174
+ schema_def = instance_eval(&blk)
175
+ parse(schema_def)
176
+ end
177
+
178
+ def parse(schema_def)
179
+ case schema_def
180
+ when VCAP::JsonSchema::BaseSchema
181
+ schema_def
182
+ when Hash
183
+ schema = VCAP::JsonSchema::HashSchema.new
184
+ for k, v in schema_def
185
+ sym = k.kind_of?(OptionalKeyMarker) ? :optional : :required
186
+ schema.send(sym, k, parse(v))
187
+ end
188
+ schema
189
+ when Array
190
+ raise SyntaxError, "Schema definition for an array must have exactly 1 element" unless schema_def.size == 1
191
+ item_schema = parse(schema_def[0])
192
+ VCAP::JsonSchema::ArraySchema.new(item_schema)
193
+ when Class
194
+ VCAP::JsonSchema::TypeSchema.new(schema_def)
195
+ else
196
+ raise SyntaxError, "Don't know what to do with class #{schema_def.class}"
197
+ end
198
+ end
199
+ end
200
+
201
+ end # VCAP::JsonSchema
202
+ end # VCAP