wakame-vdc-agents 10.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/LICENSE +202 -0
  2. data/NOTICE +1 -0
  3. data/Rakefile +142 -0
  4. data/bin/hva +972 -0
  5. data/bin/nsa +147 -0
  6. data/bin/sta +182 -0
  7. data/config/hva.conf.example +10 -0
  8. data/config/initializers/isono.rb +43 -0
  9. data/config/initializers/passenger.rb +6 -0
  10. data/config/initializers/sequel.rb +21 -0
  11. data/config/nsa.conf.example +9 -0
  12. data/config/path_resolver.rb +12 -0
  13. data/lib/dcmgr.rb +115 -0
  14. data/lib/dcmgr/endpoints/core_api.rb +1004 -0
  15. data/lib/dcmgr/endpoints/core_api_mock.rb +816 -0
  16. data/lib/dcmgr/endpoints/errors.rb +55 -0
  17. data/lib/dcmgr/endpoints/metadata.rb +129 -0
  18. data/lib/dcmgr/logger.rb +44 -0
  19. data/lib/dcmgr/models/account.rb +104 -0
  20. data/lib/dcmgr/models/account_resource.rb +16 -0
  21. data/lib/dcmgr/models/base.rb +69 -0
  22. data/lib/dcmgr/models/base_new.rb +371 -0
  23. data/lib/dcmgr/models/frontend_system.rb +38 -0
  24. data/lib/dcmgr/models/host_pool.rb +102 -0
  25. data/lib/dcmgr/models/image.rb +46 -0
  26. data/lib/dcmgr/models/instance.rb +255 -0
  27. data/lib/dcmgr/models/instance_netfilter_group.rb +16 -0
  28. data/lib/dcmgr/models/instance_nic.rb +68 -0
  29. data/lib/dcmgr/models/instance_spec.rb +21 -0
  30. data/lib/dcmgr/models/ip_lease.rb +42 -0
  31. data/lib/dcmgr/models/netfilter_group.rb +88 -0
  32. data/lib/dcmgr/models/netfilter_rule.rb +21 -0
  33. data/lib/dcmgr/models/network.rb +32 -0
  34. data/lib/dcmgr/models/physical_host.rb +67 -0
  35. data/lib/dcmgr/models/request_log.rb +25 -0
  36. data/lib/dcmgr/models/ssh_key_pair.rb +55 -0
  37. data/lib/dcmgr/models/storage_pool.rb +134 -0
  38. data/lib/dcmgr/models/tag.rb +126 -0
  39. data/lib/dcmgr/models/tag_mapping.rb +28 -0
  40. data/lib/dcmgr/models/volume.rb +130 -0
  41. data/lib/dcmgr/models/volume_snapshot.rb +47 -0
  42. data/lib/dcmgr/node_modules/hva_collector.rb +134 -0
  43. data/lib/dcmgr/node_modules/sta_collector.rb +72 -0
  44. data/lib/dcmgr/scheduler.rb +12 -0
  45. data/lib/dcmgr/scheduler/find_last.rb +16 -0
  46. data/lib/dcmgr/scheduler/find_random.rb +16 -0
  47. data/lib/dcmgr/stm/instance.rb +25 -0
  48. data/lib/dcmgr/stm/snapshot_context.rb +33 -0
  49. data/lib/dcmgr/stm/volume_context.rb +65 -0
  50. data/lib/dcmgr/web/base.rb +21 -0
  51. data/lib/sinatra/accept_media_types.rb +128 -0
  52. data/lib/sinatra/lazy_auth.rb +56 -0
  53. data/lib/sinatra/rabbit.rb +278 -0
  54. data/lib/sinatra/respond_to.rb +272 -0
  55. data/lib/sinatra/sequel_transaction.rb +27 -0
  56. data/lib/sinatra/static_assets.rb +83 -0
  57. data/lib/sinatra/url_for.rb +44 -0
  58. metadata +270 -0
@@ -0,0 +1,278 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/url_for'
3
+ require 'sinatra/respond_to'
4
+
5
+ module Sinatra
6
+ module Rabbit
7
+
8
+ class DuplicateParamException < Exception; end
9
+ class DuplicateOperationException < Exception; end
10
+ class DuplicateCollectionException < Exception; end
11
+
12
+ class Operation
13
+ attr_reader :name, :method
14
+
15
+ STANDARD = {
16
+ :index => { :method => :get, :member => false },
17
+ :show => { :method => :get, :member => true },
18
+ :create => { :method => :post, :member => false },
19
+ :update => { :method => :put, :member => true },
20
+ :destroy => { :method => :delete, :member => true }
21
+ }
22
+
23
+ def initialize(coll, name, opts, &block)
24
+ @name = name.to_sym
25
+ opts = STANDARD[@name].merge(opts) if standard?
26
+ @collection = coll
27
+ raise "No method for operation #{name}" unless opts[:method]
28
+ @method = opts[:method].to_sym
29
+ @member = opts[:member]
30
+ @description = ""
31
+ instance_eval(&block) if block_given?
32
+ end
33
+
34
+ def standard?
35
+ STANDARD.keys.include?(name)
36
+ end
37
+
38
+ def description(text="")
39
+ return @description if text.blank?
40
+ @description = text
41
+ end
42
+
43
+ def generate_documentation(app)
44
+ raise ArgumentError unless app < Sinatra::Base
45
+ coll, oper = @collection, self
46
+ app.get("/api/docs/#{@collection.name}/#{@name}") do
47
+ @collection, @operation = coll, oper
48
+ respond_to do |format|
49
+ format.html { haml :'docs/operation' }
50
+ format.xml { haml :'docs/operation' }
51
+ end
52
+ end
53
+ end
54
+
55
+ def control(&block)
56
+ op = self
57
+ @control = Proc.new do
58
+ #op.validate(params)
59
+ instance_eval(&block)
60
+ end
61
+ end
62
+
63
+ def prefix
64
+ # FIXME: Make the /api prefix configurable
65
+ "/api"
66
+ end
67
+
68
+ def path(args = {})
69
+ l_prefix = args[:prefix] ? args[:prefix] : prefix
70
+ if @member
71
+ if standard?
72
+ "#{l_prefix}/#{@collection.name}/:id"
73
+ else
74
+ "#{l_prefix}/#{@collection.name}/:id/#{name}"
75
+ end
76
+ else
77
+ "#{l_prefix}/#{@collection.name}"
78
+ end
79
+ end
80
+
81
+ def generate(app)
82
+ raise ArgumentError unless app < Sinatra::Base
83
+
84
+ app.send(@method, path, {}, &@control)
85
+ # Set up some Rails-like URL helpers
86
+ if name == :index
87
+ gen_route "#{@collection.name}_url", app
88
+ elsif name == :show
89
+ gen_route "#{@collection.name.to_s.singularize}_url", app
90
+ else
91
+ gen_route "#{name}_#{@collection.name.to_s.singularize}_url", app
92
+ end
93
+ end
94
+
95
+ def authz(&blk)
96
+ end
97
+
98
+ private
99
+ def gen_route(name, app)
100
+ route_url = path
101
+ if @member
102
+ app.send(:define_method, name) do |id, *args|
103
+ url = query_url(route_url, args[0])
104
+ url_for url.gsub(/:id/, id.to_s), :full
105
+ end
106
+ else
107
+ app.send(:define_method, name) do |*args|
108
+ url = query_url(route_url, args[0])
109
+ url_for url, :full
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ class Collection
116
+ attr_reader :name, :operations
117
+
118
+ def initialize(name, &block)
119
+ @name = name
120
+ @description = ""
121
+ @operations = {}
122
+ instance_eval(&block) if block_given?
123
+ end
124
+
125
+ # Set/Return description for collection
126
+ # If first parameter is not present, full description will be
127
+ # returned.
128
+ def description(text='')
129
+ return @description if text.blank?
130
+ @description = text
131
+ end
132
+
133
+ def generate_documentation(app)
134
+ raise ArgumentError unless app < Sinatra::Base
135
+ coll, oper, features = self, @operations, driver.features(name)
136
+ app.get("/api/docs/#{@name}") do
137
+ @collection, @operations, @features = coll, oper, features
138
+ respond_to do |format|
139
+ format.html { haml :'docs/collection' }
140
+ format.xml { haml :'docs/collection' }
141
+ end
142
+ end
143
+ end
144
+
145
+ # Add a new operation for this collection. For the standard REST
146
+ # operations :index, :show, :update, and :destroy, we already know
147
+ # what method to use and whether this is an operation on the URL for
148
+ # individual elements or for the whole collection.
149
+ #
150
+ # For non-standard operations, options must be passed:
151
+ # :method : one of the HTTP methods
152
+ # :member : whether this is an operation on the collection or an
153
+ # individual element (FIXME: custom operations on the
154
+ # collection will use a nonsensical URL) The URL for the
155
+ # operation is the element URL with the name of the operation
156
+ # appended
157
+ #
158
+ # This also defines a helper method like show_instance_url that returns
159
+ # the URL to this operation (in request context)
160
+ def operation(name, opts = {}, &block)
161
+ raise DuplicateOperationException if @operations[name]
162
+ @operations[name] = Operation.new(self, name, opts, &block)
163
+ end
164
+
165
+ def generate(app = ::Sinatra::Application)
166
+ raise ArgumentError unless app < Sinatra::Base
167
+ operations.values.each { |op| op.generate(app) }
168
+ collname = name # Work around Ruby's weird scoping/capture
169
+ app.send(:define_method, "#{name.to_s.singularize}_url") do |id|
170
+ url_for "/api/#{collname}/#{id}", :full
171
+ end
172
+
173
+ if index_op = operations[:index]
174
+ app.send(:define_method, "#{name}_url") do
175
+ url_for index_op.path.gsub(/\/\?$/,''), :full
176
+ end
177
+ end
178
+ end
179
+
180
+ def add_feature_params(features)
181
+ features.each do |f|
182
+ f.operations.each do |fop|
183
+ if cop = operations[fop.name]
184
+ fop.params.each_key do |k|
185
+ if cop.params.has_key?(k)
186
+ raise DuplicateParamException, "Parameter '#{k}' for operation #{fop.name} defined by collection #{@name} and by feature #{f.name}"
187
+ else
188
+ cop.params[k] = fop.params[k]
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ def collections
198
+ @collections ||= {}
199
+ end
200
+
201
+ # Create a new collection. NAME should be the pluralized name of the
202
+ # collection.
203
+ #
204
+ # Adds a helper method #{name}_url which returns the URL to the :index
205
+ # operation on this collection.
206
+ def collection(name, &block)
207
+ raise DuplicateCollectionException if collections[name]
208
+ #return unless driver.has_collection?(name.to_sym)
209
+ collections[name] = Collection.new(name, &block)
210
+ #collections[name].add_feature_params(driver.features(name))
211
+ collections[name].generate(self)
212
+ end
213
+
214
+ def self.registered(app)
215
+ app.register(Sinatra::RespondTo)
216
+ app.helpers RabbitHelper
217
+ app.class_eval {
218
+ # Generate a root route for API docs
219
+ get '/api/docs\/?' do
220
+ respond_to do |format|
221
+ format.html { haml :'docs/index' }
222
+ format.xml { haml :'docs/index' }
223
+ end
224
+ end
225
+ }
226
+ end
227
+ end
228
+
229
+ module RabbitHelper
230
+ def query_url(url, params)
231
+ return url if params.nil? || params.empty?
232
+ url + "?#{URI.escape(params.collect{|k,v| "#{k}=#{v}"}.join('&'))}"
233
+ end
234
+
235
+ def entry_points
236
+ collections.values.inject([]) do |m, coll|
237
+ url = url_for coll.operations[:index].path, :full
238
+ m << [ coll.name, url ]
239
+ end
240
+ end
241
+ end
242
+
243
+ end
244
+
245
+ class String
246
+ # Rails defines this for a number of other classes, including Object
247
+ # see activesupport/lib/active_support/core_ext/object/blank.rb
248
+ def blank?
249
+ self !~ /\S/
250
+ end
251
+
252
+ # Title case.
253
+ #
254
+ # "this is a string".titlecase
255
+ # => "This Is A String"
256
+ #
257
+ # CREDIT: Eliazar Parra
258
+ # Copied from facets
259
+ def titlecase
260
+ gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
261
+ end
262
+
263
+ def pluralize
264
+ self + "s"
265
+ end
266
+
267
+ def singularize
268
+ self.gsub(/s$/, '')
269
+ end
270
+
271
+ def underscore
272
+ gsub(/::/, '/').
273
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
274
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
275
+ tr("-", "_").
276
+ downcase
277
+ end
278
+ end
@@ -0,0 +1,272 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/accept_media_types'
3
+
4
+ # Accept header parsing was looked at but deemed
5
+ # too much of an irregularity to deal with. Problems with the header
6
+ # differences from IE, Firefox, Safari, and every other UA causes
7
+ # problems with the expected output. The general expected behavior
8
+ # would be serve html when no extension provided, but most UAs say
9
+ # they will accept application/xml with out a quality indicator, meaning
10
+ # you'd get the xml block served insead. Just plain retarded, use the
11
+ # extension and you'll never be suprised.
12
+
13
+ module Sinatra
14
+ module RespondTo
15
+ class UnhandledFormat < Sinatra::NotFound; end
16
+ class MissingTemplate < Sinatra::NotFound
17
+ def code; 500 end
18
+ end
19
+
20
+ TEXT_MIME_TYPES = [:txt, :html, :js, :json, :xml, :rss, :atom, :css, :asm, :c, :cc, :conf,
21
+ :csv, :cxx, :diff, :dtd, :f, :f77, :f90, :for, :gemspec, :h, :hh, :htm,
22
+ :log, :mathml, :mml, :p, :pas, :pl, :pm, :py, :rake, :rb, :rdf, :rtf, :ru,
23
+ :s, :sgm, :sgml, :sh, :svg, :svgz, :text, :wsdl, :xhtml, :xsl, :xslt, :yaml,
24
+ :yml, :ics, :png]
25
+
26
+ def self.registered(app)
27
+ app.helpers RespondTo::Helpers
28
+
29
+ app.set :default_charset, 'utf-8'
30
+ app.set :default_content, :html
31
+ app.set :assume_xhr_is_js, true
32
+
33
+ # We remove the trailing extension so routes
34
+ # don't have to be of the style
35
+ #
36
+ # get '/resouce.:format'
37
+ #
38
+ # They can instead be of the style
39
+ #
40
+ # get '/resource'
41
+ #
42
+ # and the format will automatically be available in <tt>format</tt>
43
+ app.before do
44
+ # Let through sinatra image urls in development
45
+ next if self.class.development? && request.path_info =~ %r{/__sinatra__/.*?.png}
46
+
47
+ unless options.static? && options.public? && (request.get? || request.head?) && static_file?(request.path_info)
48
+ rpi = request.path_info.sub(%r{\.([^\./]+)$}, '')
49
+
50
+ if (not $1) or ($1 and TEXT_MIME_TYPES.include?($1.to_sym))
51
+ request.path_info, ext = rpi, nil
52
+ if ! $1.nil?
53
+ ext = $1
54
+ elsif env['HTTP_ACCEPT'].nil? || env['HTTP_ACCEPT'].empty?
55
+ ext = options.default_content
56
+ end
57
+ if ext
58
+ @mime_types = [ Helpers::mime_type(ext) ]
59
+ format ext
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ app.configure :development do |dev|
66
+ dev.error UnhandledFormat do
67
+ content_type :html, :charset => 'utf-8'
68
+
69
+ (<<-HTML).gsub(/^ {10}/, '')
70
+ <!DOCTYPE html>
71
+ <html>
72
+ <head>
73
+ <style type="text/css">
74
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
75
+ color:#888;margin:20px}
76
+ #c {margin:0 auto;width:500px;text-align:left}
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <h2>Sinatra doesn't know this ditty.</h2>
81
+ <img src='/__sinatra__/404.png'>
82
+ <div id="c">
83
+ Try this:
84
+ <pre>#{request.request_method.downcase} '#{request.path_info}' do\n respond_to do |wants|\n wants.#{format} { "Hello World" }\n end\nend</pre>
85
+ </div>
86
+ </body>
87
+ </html>
88
+ HTML
89
+ end
90
+
91
+ dev.error MissingTemplate do
92
+ content_type :html, :charset => 'utf-8'
93
+ response.status = request.env['sinatra.error'].code
94
+
95
+ engine = request.env['sinatra.error'].message.split('.').last
96
+ engine = 'haml' unless ['haml', 'builder', 'erb'].include? engine
97
+
98
+ path = File.basename(request.path_info)
99
+ path = "root" if path.nil? || path.empty?
100
+
101
+ format = engine == 'builder' ? 'xml' : 'html'
102
+
103
+ layout = case engine
104
+ when 'haml' then "!!!\n%html\n %body= yield"
105
+ when 'erb' then "<html>\n <body>\n <%= yield %>\n </body>\n</html>"
106
+ when 'builder' then ::Sinatra::VERSION =~ /^1.0/ ? "xml << yield" : "builder do |xml|\n xml << yield\nend"
107
+ end
108
+
109
+ layout = "<small>app.#{format}.#{engine}</small>\n<pre>#{escape_html(layout)}</pre>"
110
+
111
+ (<<-HTML).gsub(/^ {10}/, '')
112
+ <!DOCTYPE html>
113
+ <html>
114
+ <head>
115
+ <style type="text/css">
116
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
117
+ color:#888;margin:20px}
118
+ #c {margin:0 auto;width:500px;text-align:left;}
119
+ small {float:right;clear:both;}
120
+ pre {clear:both;}
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <h2>Sinatra can't find #{request.env['sinatra.error'].message}</h2>
125
+ <img src='/__sinatra__/500.png'>
126
+ <div id="c">
127
+ Try this:<br />
128
+ #{layout}
129
+ <small>#{path}.#{format}.#{engine}</small>
130
+ <pre>Hello World!</pre>
131
+ <small>application.rb</small>
132
+ <pre>#{request.request_method.downcase} '#{request.path_info}' do\n respond_to do |wants|\n wants.#{engine == 'builder' ? 'xml' : 'html'} { #{engine} :#{path}#{",\n#{' '*32}layout => :app" if layout} }\n end\nend</pre>
133
+ </div>
134
+ </body>
135
+ </html>
136
+ HTML
137
+ end
138
+
139
+ end
140
+
141
+ app.class_eval do
142
+ private
143
+ def accept_list
144
+ @mime_types || Rack::AcceptMediaTypes.new(env['HTTP_ACCEPT'] || '')
145
+ end
146
+
147
+ # Changes in 1.0 Sinatra reuse render for layout so we store
148
+ # the original value to tell us if this is an automatic attempt
149
+ # to do a layout call. If it is, it might fail with Errno::ENOENT
150
+ # and we want to pass that back to sinatra since it isn't a MissingTemplate
151
+ # error
152
+ def render_with_format(*args, &block)
153
+ assumed_layout = args[1] == :layout
154
+ args[1] = "#{args[1]}.#{format}".to_sym if args[1].is_a?(::Symbol)
155
+ render_without_format *args, &block
156
+ rescue Errno::ENOENT => e
157
+ raise MissingTemplate, "#{args[1]}.#{args[0]}" unless assumed_layout
158
+ raise e
159
+ end
160
+ alias_method :render_without_format, :render
161
+ alias_method :render, :render_with_format
162
+
163
+ if ::Sinatra::VERSION =~ /^0\.9/
164
+ def lookup_layout_with_format(*args)
165
+ args[1] = "#{args[1]}.#{format}".to_sym if args[1].is_a?(::Symbol)
166
+ lookup_layout_without_format *args
167
+ end
168
+ alias_method :lookup_layout_without_format, :lookup_layout
169
+ alias_method :lookup_layout, :lookup_layout_with_format
170
+ end
171
+ end
172
+ end
173
+
174
+ module Helpers
175
+ # Patch the content_type function to remember the set type
176
+ # This helps cut down on time in the format helper so it
177
+ # doesn't have to do a reverse lookup on the header
178
+ def self.included(klass)
179
+ klass.class_eval do
180
+ def content_type_with_save(*args)
181
+ content_type_without_save *args
182
+ @_format = args.first.to_sym
183
+ response['Content-Type']
184
+ end
185
+ alias_method :content_type_without_save, :content_type
186
+ alias_method :content_type, :content_type_with_save
187
+ end if ::Sinatra::VERSION =~ /^1.0/
188
+ end
189
+
190
+ def self.mime_type(sym)
191
+ ::Sinatra::Base.respond_to?(:mime_type) && ::Sinatra::Base.mime_type(sym) || ::Sinatra::Base.media_type(sym)
192
+ end
193
+
194
+ def format(val=nil)
195
+ unless val.nil?
196
+ mime_type = ::Sinatra::RespondTo::Helpers.mime_type(val)
197
+ fail "Unknown media type #{val}\nTry registering the extension with a mime type" if mime_type.nil?
198
+
199
+ @_format = val.to_sym
200
+ response['Content-Type'].sub!(/^[^;]+/, mime_type)
201
+ charset options.default_charset if Sinatra::RespondTo::TEXT_MIME_TYPES.include?(format) and format!=:png
202
+ end
203
+
204
+ @_format
205
+ end
206
+
207
+ # This is mostly just a helper so request.path_info isn't changed when
208
+ # serving files from the public directory
209
+ def static_file?(path)
210
+ public_dir = File.expand_path(options.public)
211
+ path = File.expand_path(File.join(public_dir, unescape(path)))
212
+
213
+ path[0, public_dir.length] == public_dir && File.file?(path)
214
+ end
215
+
216
+ def charset(val=nil)
217
+ fail "Content-Type must be set in order to specify a charset" if response['Content-Type'].nil?
218
+
219
+ if response['Content-Type'] =~ /charset=[^;]+/
220
+ response['Content-Type'].sub!(/charset=[^;]+/, (val == '' && '') || "charset=#{val}")
221
+ else
222
+ response['Content-Type'] += ";charset=#{val}"
223
+ end unless val.nil?
224
+
225
+ response['Content-Type'][/charset=([^;]+)/, 1]
226
+ end
227
+
228
+ def respond_to(&block)
229
+ wants = Format.new
230
+ yield wants
231
+ fmt, type, handler = match_accept_type(accept_list, wants)
232
+ raise UnhandledFormat if fmt.nil?
233
+ format fmt
234
+ handler.nil? ? nil : handler.call
235
+ end
236
+
237
+ def match_accept_type(mime_types, format)
238
+ selected = []
239
+ accepted_types = mime_types.map {|type| Regexp.escape(type).gsub(/\\\*/,'.*') }
240
+ # Fix for Chrome based browsers which returns XML when 'xhtml' is requested.
241
+ if env['HTTP_USER_AGENT'] =~ /Chrome/ and accepted_types.size>1
242
+ accepted_types[0], accepted_types[1] = accepted_types[1], accepted_types[0]
243
+ if accepted_types[0].eql?('application/xhtml\\+xml')
244
+ accepted_types[0] = 'text/html'
245
+ end
246
+ end
247
+ accepted_types.each do |at|
248
+ format.each do |fmt, ht, handler|
249
+ (selected = [fmt, ht, handler]) and break if ht.match(at)
250
+ end
251
+ break unless selected.empty?
252
+ end
253
+ selected
254
+ end
255
+
256
+ # NOTE Array instead of hash because order matters (wildcard type
257
+ # matches first handler)
258
+ class Format < Array #:nodoc:
259
+ def method_missing(format, *args, &handler)
260
+ mt = Sinatra::RespondTo::Helpers.mime_type(format)
261
+ if mt.nil?
262
+ Sinatra::Base.send(:fail, "Unknown media type for respond_to: #{format}\nTry registering the extension with a mime type")
263
+ end
264
+ self << [format.to_s, mt, handler]
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+
271
+ Rack::Mime::MIME_TYPES.merge!({ ".gv" => "text/plain" })
272
+ Sinatra::Application.register Sinatra::RespondTo