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,139 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require 'rubygems'
3
+
4
+ require 'yajl'
5
+
6
+ require 'json_schema'
7
+
8
+ class JsonMessage
9
+ # Base error class that all other JsonMessage related errors should inherit from
10
+ class Error < StandardError
11
+ end
12
+
13
+ # Failed to parse json during +decode+
14
+ class ParseError < Error
15
+ end
16
+
17
+ # One or more field's values didn't match their schema
18
+ class ValidationError < Error
19
+ def initialize(field_errs)
20
+ @field_errs = field_errs
21
+ end
22
+
23
+ def to_s
24
+ err_strs = @field_errs.map{|f, e| "Field: #{f}, Error: #{e}"}
25
+ err_strs.join(', ')
26
+ end
27
+ end
28
+
29
+ class Field
30
+ attr_reader :name, :schema, :required
31
+
32
+ def initialize(name, schema, required=true)
33
+ @name = name
34
+ @schema = schema.is_a?(JsonSchema) ? schema : JsonSchema.new(schema)
35
+ @required = required
36
+ end
37
+ end
38
+
39
+ class << self
40
+ attr_reader :fields
41
+
42
+ def schema(&blk)
43
+ instance_eval &blk
44
+ end
45
+
46
+ def decode(json)
47
+ begin
48
+ dec_json = Yajl::Parser.parse(json)
49
+ rescue => e
50
+ raise ParseError, e.to_s
51
+ end
52
+
53
+ from_decoded_json(dec_json)
54
+ end
55
+
56
+ def from_decoded_json(dec_json)
57
+ raise ParseError, "Decoded JSON cannot be nil" unless dec_json
58
+
59
+ errs = {}
60
+
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.
63
+ dec_json.delete_if {|k, v| v == nil}
64
+
65
+ # Collect errors by field
66
+ @fields.each do |name, field|
67
+ 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])
71
+ elsif field.required
72
+ err = "Missing field #{name}"
73
+ end
74
+ errs[name] = err if err
75
+ end
76
+
77
+ raise ValidationError.new(errs) unless errs.empty?
78
+
79
+ new(dec_json)
80
+ end
81
+
82
+ def required(field_name, schema=JsonSchema::WILDCARD)
83
+ define_field(field_name, schema, true)
84
+ end
85
+
86
+ def optional(field_name, schema=JsonSchema::WILDCARD)
87
+ define_field(field_name, schema, false)
88
+ end
89
+
90
+ protected
91
+
92
+ def define_field(name, schema, required)
93
+ name = name.to_sym
94
+
95
+ @fields ||= {}
96
+ @fields[name] = Field.new(name, schema, required)
97
+
98
+ define_method name.to_sym do
99
+ @msg[name]
100
+ end
101
+
102
+ define_method "#{name}=".to_sym do |value|
103
+ set_field(name, value)
104
+ end
105
+ end
106
+ end
107
+
108
+ def initialize(fields={})
109
+ @msg = {}
110
+ fields.each {|k, v| set_field(k, v)}
111
+ end
112
+
113
+ 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))
118
+ end
119
+ raise ValidationError.new(missing_fields) unless missing_fields.empty?
120
+ end
121
+
122
+ Yajl::Encoder.encode(@msg)
123
+ end
124
+
125
+ def extract
126
+ @msg.dup.freeze
127
+ end
128
+
129
+ protected
130
+
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)
134
+
135
+ errs = self.class.fields[field].schema.validate(value)
136
+ raise ValidationError.new({field => errs}) if errs
137
+ @msg[field] = value
138
+ end
139
+ end
@@ -0,0 +1,84 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ # This class provides dead simple declarative validation for decoded json using
3
+ # a fairly intuitive DSL like syntax.
4
+ #
5
+ # For example, the following is a sample schema that exercises all functionality
6
+ #
7
+ # {'foo' => [String], # 'foo' must be a list of strings
8
+ # 'bar' => {'baz' => Fixnum, # 'bar' must be a hash where
9
+ # 'jaz' => /foo/, # 'baz' is a Fixnum, and
10
+ # } # 'jaz' matches the regex /foo/
11
+ # }
12
+ #
13
+ class JsonSchema
14
+ WILDCARD = Object
15
+
16
+ # TODO(mjp): validate that schema is syntatically correct
17
+
18
+ def initialize(schema)
19
+ @schema = schema
20
+ end
21
+
22
+ def validate(json)
23
+ _validate(json, @schema)
24
+ end
25
+
26
+ protected
27
+
28
+ def _validate(json, schema)
29
+ case schema
30
+ when Class
31
+ # Terminal case, type check
32
+ klass = json.class
33
+ if json.is_a? schema
34
+ nil
35
+ else
36
+ "Type mismatch (expected #{schema}, got #{klass})"
37
+ end
38
+
39
+ when Hash
40
+ # Recursive case, check for required params, recursively check them against the supplied schema
41
+ missing_keys = schema.keys.select {|k| !json.has_key?(k)} if json.is_a? Hash
42
+
43
+ if !(json.is_a? Hash)
44
+ "Type mismatch (expected hash, got #{json.class})"
45
+ elsif missing_keys.length > 0
46
+ "Missing params: '#{missing_keys.join(', ')}'"
47
+ else
48
+ errs = nil
49
+ schema.each_key do |k|
50
+ sub_errs = _validate(json[k], schema[k])
51
+ if sub_errs
52
+ errs ||= {}
53
+ errs[k] = sub_errs
54
+ end
55
+ end
56
+ errs
57
+ end
58
+
59
+ when Array
60
+ # Recursive case, check that array isn't empty, recursively check array against supplied schema
61
+ if !(json.is_a? Array)
62
+ "Type mismatch (expected array, got #{json.class}"
63
+ else
64
+ errs = nil
65
+ json.each do |v|
66
+ errs = _validate(v, schema[0])
67
+ break if errs
68
+ end
69
+ errs
70
+ end
71
+
72
+ when Regexp
73
+ if schema.match(json)
74
+ nil
75
+ else
76
+ "Invalid value (doesn't match '#{schema.source})"
77
+ end
78
+
79
+ else
80
+ # Terminal case, value check
81
+ "Value mismatch (expected '#{schema}', got #{json})" unless json == schema
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,43 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require 'eventmachine'
3
+ require 'em-http-request'
4
+ require 'fiber'
5
+
6
+ require 'services/api/const'
7
+
8
+ module VCAP
9
+ module Services
10
+ module Api
11
+ end
12
+ end
13
+ end
14
+
15
+ module VCAP::Services::Api
16
+ class AsyncHttpRequest
17
+ class << self
18
+ def new(url, token, verb, timeout, msg=VCAP::Services::Api::EMPTY_REQUEST)
19
+
20
+ req = {
21
+ :head => {
22
+ VCAP::Services::Api::GATEWAY_TOKEN_HEADER => token,
23
+ 'Content-Type' => 'application/json',
24
+ },
25
+ :body => msg.encode,
26
+ }
27
+ if timeout
28
+ EM::HttpRequest.new(url, :inactivity_timeout => timeout).send(verb.to_sym, req)
29
+ else
30
+ EM::HttpRequest.new(url).send(verb.to_sym, req)
31
+ end
32
+ end
33
+
34
+ def fibered(url, token, verb, timeout, msg=VCAP::Services::Api::EMPTY_REQUEST)
35
+ req = new(url, token, verb, timeout, msg)
36
+ f = Fiber.current
37
+ req.callback { f.resume(req) }
38
+ req.errback { f.resume(req) }
39
+ Fiber.yield
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,107 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require 'net/http'
3
+
4
+ require 'services/api/const'
5
+ require 'services/api/messages'
6
+
7
+ module VCAP
8
+ module Services
9
+ module Api
10
+ end
11
+ end
12
+ end
13
+
14
+ class VCAP::Services::Api::ServiceGatewayClient
15
+
16
+ class UnexpectedResponse < StandardError
17
+ attr_reader :response
18
+
19
+ def initialize(resp)
20
+ @response = resp
21
+ end
22
+ end
23
+
24
+ attr_reader :host, :port, :token
25
+
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
35
+
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
41
+
42
+ def unprovision(args)
43
+ perform_request(Net::HTTP::Delete, "/gateway/v1/configurations/#{args[:service_id]}")
44
+ end
45
+
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
50
+
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
55
+
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
60
+
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
65
+
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
70
+
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
75
+
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
80
+
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
85
+
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
91
+
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
96
+
97
+ protected
98
+
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
106
+
107
+ end
@@ -0,0 +1,9 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ module VCAP
3
+ module Services
4
+ module Api
5
+ GATEWAY_TOKEN_HEADER = 'X-VCAP-Service-Token'
6
+ SERVICE_LABEL_REGEX = /^\S+-\S+$/
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,153 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require 'uri'
3
+
4
+ require 'services/api/const'
5
+ require 'json_message'
6
+
7
+ module VCAP
8
+ module Services
9
+ module Api
10
+
11
+ class EmptyRequest < JsonMessage
12
+ end
13
+ EMPTY_REQUEST = EmptyRequest.new.freeze
14
+
15
+ #
16
+ # Tell the CloudController about a service
17
+ # NB: Deleting an offering takes all args in the url
18
+ #
19
+ class ServiceOfferingRequest < JsonMessage
20
+ required :label, SERVICE_LABEL_REGEX
21
+ required :url, URI::regexp(%w(http https))
22
+
23
+ optional :description, String
24
+ optional :info_url, URI::regexp(%w(http https))
25
+ optional :tags, [String]
26
+ optional :plans, [String]
27
+ optional :plan_options
28
+ optional :binding_options
29
+ optional :acls
30
+ optional :active
31
+ optional :timeout, Integer
32
+ end
33
+
34
+ class BrokeredServiceOfferingRequest < JsonMessage
35
+ required :label, SERVICE_LABEL_REGEX
36
+ required :options, [{"name" => String, "credentials" => Hash}]
37
+ optional :description, String
38
+ end
39
+
40
+ class HandleUpdateRequest < JsonMessage
41
+ required :service_id, String
42
+ required :configuration
43
+ required :credentials
44
+ end
45
+
46
+ class ListHandlesResponse < JsonMessage
47
+ required :handles, [::JsonSchema::WILDCARD]
48
+ end
49
+
50
+ class ListBrokeredServicesResponse < JsonMessage
51
+ required :brokered_services, [{"label" => String, "description" => String, "acls" => {"users" => [String], "wildcards" => [String]}}]
52
+ end
53
+
54
+ #
55
+ # Provision a service instance
56
+ # NB: Unprovision takes all args in the url
57
+ #
58
+ class CloudControllerProvisionRequest < JsonMessage
59
+ required :label, SERVICE_LABEL_REGEX
60
+ required :name, String
61
+ required :plan, String
62
+
63
+ optional :plan_option
64
+ end
65
+
66
+ class GatewayProvisionRequest < JsonMessage
67
+ required :label, SERVICE_LABEL_REGEX
68
+ required :name, String
69
+ required :plan, String
70
+ required :email, String
71
+
72
+ optional :plan_option
73
+ end
74
+
75
+ class GatewayProvisionResponse < JsonMessage
76
+ required :service_id, String
77
+ required :data
78
+ required :credentials
79
+ end
80
+
81
+ #
82
+ # Bind a previously provisioned service to an app
83
+ #
84
+ class CloudControllerBindRequest < JsonMessage
85
+ required :service_id, String
86
+ required :app_id, Integer
87
+ required :binding_options
88
+ end
89
+
90
+ class GatewayBindRequest < JsonMessage
91
+ required :service_id, String
92
+ required :label, String
93
+ required :email, String
94
+ required :binding_options
95
+ end
96
+
97
+ class GatewayUnbindRequest < JsonMessage
98
+ required :service_id, String
99
+ required :handle_id, String
100
+ required :binding_options
101
+ end
102
+
103
+ class CloudControllerBindResponse < JsonMessage
104
+ required :label, SERVICE_LABEL_REGEX
105
+ required :binding_token, String
106
+ end
107
+
108
+ class GatewayBindResponse < JsonMessage
109
+ required :service_id, String
110
+ required :configuration
111
+ required :credentials
112
+ end
113
+
114
+ # Bind app_name using binding_token
115
+ class BindExternalRequest < JsonMessage
116
+ required :binding_token, String
117
+ required :app_id, Integer
118
+ end
119
+
120
+ class BindingTokenRequest < JsonMessage
121
+ required :service_id, String
122
+ required :binding_options
123
+ end
124
+
125
+ class Snapshot < JsonMessage
126
+ required :snapshot_id, String
127
+ required :date, String
128
+ required :size, Integer
129
+ end
130
+
131
+ class SnapshotList < JsonMessage
132
+ required :snapshots, [::JsonSchema::WILDCARD]
133
+ end
134
+
135
+ class Job < JsonMessage
136
+ required :job_id, String
137
+ required :status, String
138
+ required :start_time, String
139
+ optional :description, String
140
+ optional :complete_time, String
141
+ optional :result, ::JsonSchema::WILDCARD
142
+ end
143
+
144
+ class SerializedURL < JsonMessage
145
+ required :url, URI::regexp(%w(http https))
146
+ end
147
+
148
+ class SerializedData < JsonMessage
149
+ required :data, String
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,17 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ module VCAP
3
+ module Services
4
+ module Api
5
+ end
6
+ end
7
+ end
8
+
9
+ class VCAP::Services::Api::Util
10
+ class << self
11
+ def parse_label(label)
12
+ raise ArgumentError, "Invalid label" unless label.match(/-/)
13
+ name, _, version = label.rpartition(/-/)
14
+ [name, version]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2009-2011 VMware, Inc.
2
+ require 'services/api/async_requests'
3
+ require 'services/api/const'
4
+ require 'services/api/messages'
5
+ require 'services/api/clients/service_gateway_client'
6
+ require 'services/api/util'