vcap_common 1.0.10 → 2.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/json_message.rb CHANGED
@@ -1,46 +1,67 @@
1
- # Copyright (c) 2009-2011 VMware, Inc.
1
+ # Copyright (c) 2009-2011 VMware, Inc
2
2
  require 'rubygems'
3
-
4
3
  require 'yajl'
5
-
6
- require 'json_schema'
4
+ require 'membrane'
7
5
 
8
6
  class JsonMessage
9
- # Base error class that all other JsonMessage related errors should inherit from
7
+ # Base error class that all other JsonMessage related errors should
8
+ # inherit from
10
9
  class Error < StandardError
11
10
  end
12
11
 
12
+ # Fields not defined properly.
13
+ class DefinitionError < Error
14
+ end
15
+
13
16
  # Failed to parse json during +decode+
14
17
  class ParseError < Error
15
18
  end
16
19
 
17
20
  # One or more field's values didn't match their schema
18
21
  class ValidationError < Error
19
- def initialize(field_errs)
20
- @field_errs = field_errs
22
+ attr_reader :errors
23
+
24
+ def initialize(errors)
25
+ @errors = errors
21
26
  end
22
27
 
23
28
  def to_s
24
- err_strs = @field_errs.map{|f, e| "Field: #{f}, Error: #{e}"}
29
+ err_strs = @errors.map { |f, e| "Field: #{f}, Error: #{e}" }
25
30
  err_strs.join(', ')
26
31
  end
27
32
  end
28
33
 
29
34
  class Field
30
- attr_reader :name, :schema, :required
35
+ attr_reader :name, :schema, :required, :default
36
+
37
+ def initialize(name, options = {}, &blk)
38
+ blk ||= lambda { |*_| options[:schema] || any }
31
39
 
32
- def initialize(name, schema, required=true)
33
40
  @name = name
34
- @schema = schema.is_a?(JsonSchema) ? schema : JsonSchema.new(schema)
35
- @required = required
41
+ @schema = Membrane::SchemaParser.parse(&blk)
42
+ @required = options[:required] || false
43
+ @default = options[:default]
44
+
45
+ if @required && @default
46
+ raise DefinitionError, \
47
+ "Cannot define a default value for required field #{name}"
48
+ end
49
+
50
+ validate(@default) if @default
51
+ end
52
+
53
+ def validate(value)
54
+ begin
55
+ @schema.validate(value)
56
+ rescue Membrane::SchemaValidationError => e
57
+ raise ValidationError.new( { name => e.message } )
58
+ end
36
59
  end
37
60
  end
38
61
 
39
62
  class << self
40
- attr_reader :fields
41
-
42
- def schema(&blk)
43
- instance_eval &blk
63
+ def fields
64
+ @fields ||= {}
44
65
  end
45
66
 
46
67
  def decode(json)
@@ -58,19 +79,23 @@ class JsonMessage
58
79
 
59
80
  errs = {}
60
81
 
61
- # Treat null values as if the keys aren't present. This isn't as strict as one would like,
62
- # but conforms to typical use cases.
82
+ # Treat null values as if the keys aren't present. This isn't as strict
83
+ # as one would like, but conforms to typical use cases.
63
84
  dec_json.delete_if {|k, v| v == nil}
64
85
 
65
86
  # Collect errors by field
66
- @fields.each do |name, field|
87
+ fields.each do |name, field|
67
88
  err = nil
68
- name_s = name.to_s
69
- if dec_json.has_key?(name_s)
70
- err = field.schema.validate(dec_json[name_s])
89
+ if dec_json.has_key?(name.to_s)
90
+ begin
91
+ field.validate(dec_json[name.to_s])
92
+ rescue ValidationError => e
93
+ err = e.errors[name]
94
+ end
71
95
  elsif field.required
72
96
  err = "Missing field #{name}"
73
97
  end
98
+
74
99
  errs[name] = err if err
75
100
  end
76
101
 
@@ -79,27 +104,27 @@ class JsonMessage
79
104
  new(dec_json)
80
105
  end
81
106
 
82
- def required(field_name, schema=JsonSchema::WILDCARD)
83
- define_field(field_name, schema, true)
107
+ def required(name, schema = nil, &blk)
108
+ define_field(name, :schema => schema, :required => true, &blk)
84
109
  end
85
110
 
86
- def optional(field_name, schema=JsonSchema::WILDCARD)
87
- define_field(field_name, schema, false)
111
+ def optional(name, schema = nil, default = nil, &blk)
112
+ define_field(name, :schema => schema, :default => default, &blk)
88
113
  end
89
114
 
90
115
  protected
91
116
 
92
- def define_field(name, schema, required)
117
+ def define_field(name, options = {}, &blk)
93
118
  name = name.to_sym
94
119
 
95
- @fields ||= {}
96
- @fields[name] = Field.new(name, schema, required)
120
+ fields[name] = Field.new(name, options, &blk)
97
121
 
98
- define_method name.to_sym do
122
+ define_method(name) do
123
+ set_default(name)
99
124
  @msg[name]
100
125
  end
101
126
 
102
- define_method "#{name}=".to_sym do |value|
127
+ define_method("#{name}=") do |value|
103
128
  set_field(name, value)
104
129
  end
105
130
  end
@@ -107,33 +132,61 @@ class JsonMessage
107
132
 
108
133
  def initialize(fields={})
109
134
  @msg = {}
110
- fields.each {|k, v| set_field(k, v)}
135
+ fields.each { |name, value| set_field(name, value) }
136
+ set_defaults
111
137
  end
112
138
 
113
139
  def encode
114
- if self.class.fields
115
- missing_fields = {}
116
- self.class.fields.each do |name, field|
117
- missing_fields[name] = "Missing field #{name}" unless (!field.required || @msg.has_key?(name))
140
+ set_defaults
141
+
142
+ missing_fields = {}
143
+
144
+ self.class.fields.each do |name, field|
145
+ if field.required && !@msg.has_key?(name)
146
+ missing_fields[name] = "Missing field #{name}"
118
147
  end
119
- raise ValidationError.new(missing_fields) unless missing_fields.empty?
120
148
  end
121
149
 
150
+ raise ValidationError.new(missing_fields) unless missing_fields.empty?
151
+
122
152
  Yajl::Encoder.encode(@msg)
123
153
  end
124
154
 
125
- def extract
126
- @msg.dup.freeze
155
+ def extract(opts = {})
156
+ hash = @msg.dup
157
+ if opts[:stringify_keys]
158
+ hash = hash.inject({}) { |memo,(k,v)| memo[k.to_s] = v; memo }.freeze
159
+ end
160
+
161
+ hash.freeze
127
162
  end
128
163
 
129
164
  protected
130
165
 
131
- def set_field(field, value)
132
- field = field.to_sym
133
- raise ValidationError.new({field => "Unknown field #{field}"}) unless self.class.fields.has_key?(field)
166
+ def set_field(name, value)
167
+ name = name.to_sym
168
+ field = self.class.fields[name]
169
+
170
+ unless field
171
+ raise ValidationError.new( { name => "Unknown field: #{name}" } )
172
+ end
134
173
 
135
- errs = self.class.fields[field].schema.validate(value)
136
- raise ValidationError.new({field => errs}) if errs
137
- @msg[field] = value
174
+ field.validate(value)
175
+ @msg[name] = value
176
+ end
177
+
178
+ def set_defaults
179
+ self.class.fields.each do |name, _|
180
+ set_default(name)
181
+ end
182
+ end
183
+
184
+ def set_default(name)
185
+ unless @msg.has_key?(name)
186
+ field = self.class.fields[name]
187
+ if field
188
+ @msg[name] = field.default if field.default
189
+ end
190
+ end
138
191
  end
139
192
  end
data/lib/services/api.rb CHANGED
@@ -3,4 +3,5 @@ require 'services/api/async_requests'
3
3
  require 'services/api/const'
4
4
  require 'services/api/messages'
5
5
  require 'services/api/clients/service_gateway_client'
6
+ require 'services/api/clients/sds_client'
6
7
  require 'services/api/util'
@@ -2,6 +2,7 @@
2
2
  require 'eventmachine'
3
3
  require 'em-http-request'
4
4
  require 'fiber'
5
+ require 'httpclient'
5
6
 
6
7
  require 'services/api/const'
7
8
 
@@ -31,11 +32,54 @@ module VCAP::Services::Api
31
32
  end
32
33
  end
33
34
 
34
- def fibered(url, token, verb, timeout, msg=VCAP::Services::Api::EMPTY_REQUEST)
35
+ def request(url, token, verb, timeout, msg=VCAP::Services::Api::EMPTY_REQUEST)
35
36
  req = new(url, token, verb, timeout, msg)
36
37
  f = Fiber.current
37
38
  req.callback { f.resume(req) }
38
39
  req.errback { f.resume(req) }
40
+ http = Fiber.yield
41
+ raise UnexpectedResponse, "Error sending request #{msg.extract.to_json} to gateway #{@url}: #{http.error}" unless http.error.empty?
42
+ code = http.response_header.status.to_i
43
+ body = http.response
44
+ [code, body]
45
+ end
46
+ end
47
+ end
48
+
49
+ module SynchronousHttpRequest
50
+ def self.request(url, token, verb, timeout, msg=VCAP::Services::Api::EMPTY_REQUEST)
51
+ header = {
52
+ VCAP::Services::Api::GATEWAY_TOKEN_HEADER => token,
53
+ 'Content-Type' => 'application/json',
54
+ }
55
+ body = msg.encode
56
+ client = HTTPClient.new
57
+ msg = client.request(verb.to_sym, url, :body => body, :header => header)
58
+ [msg.code, msg.body]
59
+ end
60
+ end
61
+
62
+ class AsyncHttpMultiPartUpload
63
+ class << self
64
+ def new(url, timeout, multipart, head={})
65
+ req = {
66
+ :head => head,
67
+ :body => "",
68
+ :multipart => multipart
69
+ }
70
+
71
+ if timeout
72
+ EM::HttpRequest.new(url, :inactivity_timeout => timeout).post req
73
+ else
74
+ EM::HttpRequest.new(url).post req
75
+ end
76
+ end
77
+
78
+ def fibered(url, timeout, multipart, head={})
79
+ req = new(url, timeout, multipart, head)
80
+ f = Fiber.current
81
+ req.callback { f.resume(req) }
82
+ req.errback {f.resume(req)}
39
83
  Fiber.yield
40
84
  end
41
85
  end
@@ -0,0 +1,84 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+ require 'net/http'
3
+ require 'net/http/post/multipart'
4
+ require 'mime/types'
5
+ require 'uri'
6
+
7
+ require 'services/api/const'
8
+ require 'services/api/messages'
9
+ require 'services/api/multipart'
10
+
11
+ module VCAP
12
+ module Services
13
+ module Api
14
+ end
15
+ end
16
+ end
17
+
18
+ module VCAP::Services::Api
19
+ class SDSClient
20
+
21
+ class SDSErrorResponse < StandardError; end
22
+ class UnexpectedResponse < StandardError; end
23
+
24
+ def initialize(url, upload_token, timeout=60, opts={})
25
+ @url = url
26
+ @timeout = timeout
27
+ @hdrs = {
28
+ 'Content-Type' => 'application/json',
29
+ }
30
+ @upload_hdrs = {
31
+ 'Content-Type' => 'multipart/form-data',
32
+ SDS_UPLOAD_TOKEN_HEADER => upload_token
33
+ }
34
+ end
35
+
36
+ def import_from_data(args)
37
+ resp = perform_multipart_upload("/serialized/#{args[:service]}/#{args[:service_id]}/serialized/data", args[:msg])
38
+ SerializedURL.decode(resp)
39
+ end
40
+
41
+ protected
42
+
43
+ def perform_multipart_upload(path, file_path)
44
+ # upload file using multipart/form data
45
+ result = nil
46
+ uri = URI.parse(@url)
47
+
48
+ mime_types = MIME::Types.type_for(file_path) || []
49
+ mime_types << "application/octet-stream" if mime_types.empty?
50
+
51
+ if EM.reactor_running?
52
+ payload = {:_method => 'put', :data_file => EM::StreamUploadIO.new(file_path, mime_types[0])}
53
+ multipart = EM::Multipart.new(payload, @upload_hdrs)
54
+ url = URI.parse(uri.to_s + path)
55
+ http = AsyncHttpMultiPartUpload.fibered(url, @timeout, multipart)
56
+ raise UnexpectedResponse, "Error uploading #{file_path} to serialized_data_server #{@url}: #{http.error}" unless http.error.empty?
57
+ code = http.response_header.status.to_i
58
+ body = http.response
59
+ else
60
+ payload = {:_method => 'put', :data_file => UploadIO.new(file_path, mime_types[0])}
61
+ req = Net::HTTP::Post::Multipart.new(path, payload, @upload_hdrs)
62
+ resp = Net::HTTP.new(uri.host, uri.port).start do |http|
63
+ http.request(req)
64
+ end
65
+ code = resp.code.to_i
66
+ body = resp.body
67
+ end
68
+ case code
69
+ when 200
70
+ body
71
+ when 400
72
+ raise SDSErrorResponse, "Fail to upload the file to serialization_data_server."
73
+ when 403
74
+ raise SDSErrorResponse, "You are forbidden to access serialization_data_server."
75
+ when 404
76
+ raise SDSErrorResponse, "Not found in serialization_data_server."
77
+ when 501
78
+ raise SDSErrorResponse, "Serialized data file is recognized, but file not found in serialization_data_server."
79
+ else
80
+ raise UnexpectedResponse, "Unexpected exception in serialization_data_server: #{(uri.to_s + path)} #{code} #{body}"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,8 +1,10 @@
1
1
  # Copyright (c) 2009-2011 VMware, Inc.
2
2
  require 'net/http'
3
+ require 'uri'
3
4
 
4
5
  require 'services/api/const'
5
6
  require 'services/api/messages'
7
+ require 'services/api/async_requests'
6
8
 
7
9
  module VCAP
8
10
  module Services
@@ -11,97 +13,172 @@ module VCAP
11
13
  end
12
14
  end
13
15
 
14
- class VCAP::Services::Api::ServiceGatewayClient
16
+ module VCAP::Services::Api
17
+ class ServiceGatewayClient
18
+ METHODS_MAP = {
19
+ :get => Net::HTTP::Get,
20
+ :post=> Net::HTTP::Post,
21
+ :put => Net::HTTP::Put,
22
+ :delete => Net::HTTP::Delete,
23
+ }
15
24
 
16
- class UnexpectedResponse < StandardError
17
- attr_reader :response
25
+ # Public: Indicate gateway client encounter an unexpected error,
26
+ # such as can't connect to gateway or can't decode response.
27
+ #
28
+ class UnexpectedResponse < StandardError; end
29
+
30
+ # Public: Indicate an error response from gateway
31
+ #
32
+ class ErrorResponse < StandardError
33
+ attr_reader :status, :error
34
+
35
+ # status - the http status
36
+ # error - a ServiceErrorResponse object
37
+ #
38
+ def initialize(status, error)
39
+ @status = status
40
+ @error = error
41
+ end
42
+
43
+ def to_s
44
+ "Reponse status:#{status},error:[#{error.extract}]"
45
+ end
46
+ end
18
47
 
19
- def initialize(resp)
20
- @response = resp
48
+ class NotFoundResponse < ErrorResponse
49
+ def initialize(error)
50
+ super(404, error)
51
+ end
21
52
  end
22
- end
23
53
 
24
- attr_reader :host, :port, :token
54
+ class GatewayInternalResponse < ErrorResponse
55
+ def initialize(error)
56
+ super(503, error)
57
+ end
58
+ end
25
59
 
26
- def initialize(host, token, port=80)
27
- @host = host
28
- @port = port
29
- @token = token
30
- @hdrs = {
31
- 'Content-Type' => 'application/json',
32
- VCAP::Services::Api::GATEWAY_TOKEN_HEADER => @token
33
- }
34
- end
60
+ attr_reader :host, :port, :token
61
+ attr_reader :requester
62
+ def initialize(url, token, timeout, opts={})
63
+ @url = url
64
+ @timeout = timeout
65
+ @token = token
66
+ @hdrs = {
67
+ 'Content-Type' => 'application/json',
68
+ GATEWAY_TOKEN_HEADER => @token
69
+ }
70
+ # contract: @requester.responds_to? :request(url, token, timeout, [msg])
71
+ # contract @requester.request(url, token, timeout, [msg]) => [code, body]
72
+ @requester = opts[:requester] || AsyncHttpRequest
73
+ end
35
74
 
36
- def provision(args)
37
- msg = VCAP::Services::Api::GatewayProvisionRequest.new(args)
38
- resp = perform_request(Net::HTTP::Post, '/gateway/v1/configurations', msg)
39
- VCAP::Services::Api::GatewayProvisionResponse.decode(resp.body)
40
- end
75
+ def provision(args)
76
+ msg = GatewayProvisionRequest.new(args)
77
+ resp = perform_request(:post, '/gateway/v1/configurations', msg)
78
+ GatewayHandleResponse.decode(resp)
79
+ end
41
80
 
42
- def unprovision(args)
43
- perform_request(Net::HTTP::Delete, "/gateway/v1/configurations/#{args[:service_id]}")
44
- end
81
+ def unprovision(args)
82
+ resp = perform_request(:delete, "/gateway/v1/configurations/#{args[:service_id]}")
83
+ EMPTY_REQUEST
84
+ end
45
85
 
46
- def create_snapshot(args)
47
- resp = perform_request(Net::HTTP::Post, "/gateway/v1/configurations/#{args[:service_id]}/snapshots")
48
- VCAP::Services::Api::Job.decode(resp.body)
49
- end
86
+ def create_snapshot(args)
87
+ resp = perform_request(:post, "/gateway/v1/configurations/#{args[:service_id]}/snapshots")
88
+ Job.decode(resp)
89
+ end
50
90
 
51
- def enum_snapshots(args)
52
- resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/snapshots")
53
- VCAP::Services::Api::SnapshotList.decode(resp.body)
54
- end
91
+ def enum_snapshots(args)
92
+ resp = perform_request(:get, "/gateway/v1/configurations/#{args[:service_id]}/snapshots")
93
+ SnapshotList.decode(resp)
94
+ end
55
95
 
56
- def snapshot_details(args)
57
- resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}")
58
- VCAP::Services::Api::Snapshot.decode(resp.body)
59
- end
96
+ def snapshot_details(args)
97
+ resp = perform_request(:get, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}")
98
+ Snapshot.decode(resp)
99
+ end
60
100
 
61
- def rollback_snapshot(args)
62
- resp = perform_request(Net::HTTP::Put, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}")
63
- VCAP::Services::Api::Job.decode(resp.body)
64
- end
101
+ def update_snapshot_name(args)
102
+ perform_request(:post, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}/name", args[:msg])
103
+ EMPTY_REQUEST
104
+ end
65
105
 
66
- def serialized_url(args)
67
- resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/serialized/url")
68
- VCAP::Services::Api::Job.decode(resp.body)
69
- end
106
+ def rollback_snapshot(args)
107
+ resp = perform_request(:put, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}")
108
+ Job.decode(resp)
109
+ end
70
110
 
71
- def import_from_url(args)
72
- resp = perform_request(Net::HTTP::Put, "/gateway/v1/configurations/#{args[:service_id]}/serialized/url", args[:msg])
73
- VCAP::Services::Api::Job.decode(resp.body)
74
- end
111
+ def delete_snapshot(args)
112
+ resp = perform_request(:delete, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}")
113
+ Job.decode(resp)
114
+ end
75
115
 
76
- def import_from_data(args)
77
- resp = perform_request(Net::HTTP::Put, "/gateway/v1/configurations/#{args[:service_id]}/serialized/data", args[:msg])
78
- VCAP::Services::Api::Job.decode(resp.body)
79
- end
116
+ def create_serialized_url(args)
117
+ resp = perform_request(:post, "/gateway/v1/configurations/#{args[:service_id]}/serialized/url/snapshots/#{args[:snapshot_id]}")
118
+ Job.decode(resp)
119
+ end
80
120
 
81
- def job_info(args)
82
- resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/jobs/#{args[:job_id]}")
83
- VCAP::Services::Api::Job.decode(resp.body)
84
- end
121
+ def serialized_url(args)
122
+ resp = perform_request(:get, "/gateway/v1/configurations/#{args[:service_id]}/serialized/url/snapshots/#{args[:snapshot_id]}")
123
+ SerializedURL.decode(resp)
124
+ end
85
125
 
86
- def bind(args)
87
- msg = VCAP::Services::Api::GatewayBindRequest.new(args)
88
- resp = perform_request(Net::HTTP::Post, "/gateway/v1/configurations/#{msg.service_id}/handles", msg)
89
- VCAP::Services::Api::GatewayBindResponse.decode(resp.body)
90
- end
126
+ def import_from_url(args)
127
+ resp = perform_request(:put, "/gateway/v1/configurations/#{args[:service_id]}/serialized/url", args[:msg])
128
+ Job.decode(resp)
129
+ end
91
130
 
92
- def unbind(args)
93
- msg = VCAP::Services::Api::GatewayUnbindRequest.new(args)
94
- perform_request(Net::HTTP::Delete, "/gateway/v1/configurations/#{msg.service_id}/handles/#{msg.handle_id}", msg)
95
- end
131
+ def job_info(args)
132
+ resp = perform_request(:get, "/gateway/v1/configurations/#{args[:service_id]}/jobs/#{args[:job_id]}")
133
+ Job.decode(resp)
134
+ end
96
135
 
97
- protected
136
+ def bind(args)
137
+ msg = GatewayBindRequest.new(args)
138
+ resp = perform_request(:post, "/gateway/v1/configurations/#{msg.service_id}/handles", msg)
139
+ GatewayHandleResponse.decode(resp)
140
+ end
98
141
 
99
- def perform_request(klass, path, msg=VCAP::Services::Api::EMPTY_REQUEST)
100
- req = klass.new(path, initheader=@hdrs)
101
- req.body = msg.encode
102
- resp = Net::HTTP.new(@host, @port).start {|http| http.request(req)}
103
- raise UnexpectedResponse, resp unless resp.is_a? Net::HTTPOK
104
- resp
105
- end
142
+ def unbind(args)
143
+ msg = GatewayUnbindRequest.new(args)
144
+ perform_request(:delete, "/gateway/v1/configurations/#{msg.service_id}/handles/#{msg.handle_id}", msg)
145
+ EMPTY_REQUEST
146
+ end
106
147
 
148
+ protected
149
+
150
+ def perform_request(http_method, path, msg=VCAP::Services::Api::EMPTY_REQUEST)
151
+ result = nil
152
+ uri = URI.parse(@url)
153
+ if EM.reactor_running?
154
+ url = URI.parse(uri.to_s + path)
155
+ code, body = requester.request(url, @token, http_method, @timeout, msg)
156
+ else
157
+ klass = METHODS_MAP[http_method]
158
+ req = klass.new(path, initheader=@hdrs)
159
+ req.body = msg.encode
160
+ resp = Net::HTTP.new(uri.host, uri.port).start {|http| http.request(req)}
161
+ code = resp.code.to_i
162
+ body = resp.body
163
+ end
164
+ case code
165
+ when 200
166
+ body
167
+ when 404
168
+ err = ServiceErrorResponse.decode(body)
169
+ raise NotFoundResponse.new(err)
170
+ when 503
171
+ err = ServiceErrorResponse.decode(body)
172
+ raise GatewayInternalResponse.new(err)
173
+ else
174
+ begin
175
+ # try to decode the response
176
+ err = ServiceErrorResponse.decode(body)
177
+ rescue => e
178
+ raise UnexpectedResponse, "Can't decode gateway response. status code:#{code}, response body:#{body}"
179
+ end
180
+ raise ErrorResponse.new(code, err)
181
+ end
182
+ end
183
+ end
107
184
  end