vcap_common 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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