restfully 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,5 @@
1
1
  Copyright (c) 2009 Cyril Rohr
2
+ Copyright (c) 2009 INRIA Rennes - Bretagne Atlantique
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining
4
5
  a copy of this software and associated documentation files (the
@@ -9,7 +9,8 @@ Alpha work.
9
9
 
10
10
  == Usage
11
11
  === Command line
12
- $ resfully base_uri [root_path] [-u username] [-p password]
12
+ $ export RUBYOPT="-rubygems"
13
+ $ restfully base_uri [root_path] [-u username] [-p password]
13
14
 
14
15
  e.g., for the Grid5000 API:
15
16
  $ restfully https://api.grid5000.fr/sid /grid5000 -u username -p password
@@ -17,37 +18,74 @@ e.g., for the Grid5000 API:
17
18
  If the connection was successful, you should get a prompt. Call the +root+ function to see what are the available resources. <tt>@property</tt> means that you can call the +property+ method on the object. Other properties are available via the <tt>[]</tt> function.
18
19
  e.g.:
19
20
  irb(main):005:0> root
20
- => #<Restfully::Resource:0x90bdd4
21
- ------------ META ------------
22
- @uri: "/grid5000"
23
- @uid: "grid5000"
24
- @type: "grid"
25
- @environments: Restfully::Collection
26
- @sites: Restfully::Collection
27
- @version: Restfully::Resource
28
- @versions: Restfully::Collection
29
- ------------ PROPERTIES ------------
30
- "version" => "ee1f8111c8835496a9e4a6f7c9491c0238ee9827">
21
+ => #<Restfully::Resource:0x8848de
22
+ ------------ META ------------
23
+ @uri: #<URI::Generic:0x11091a8 URL:/grid5000>
24
+ @uid: "grid5000"
25
+ @type: "grid"
26
+ @environments: Restfully::Collection
27
+ @sites: Restfully::Collection
28
+ @version: Restfully::Resource
29
+ @versions: Restfully::Collection
30
+ ------------ PROPERTIES ------------
31
+ "version" => "4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8">
31
32
 
32
33
  irb(main):006:0> root.uri
33
- => "/grid5000"
34
+ => #<URI::Generic:0x11091a8 URL:/grid5000>
34
35
 
35
36
  irb(main):007:0> root.sites
36
- => #<Restfully::Collection:0x8faaf2
37
- ------------ META ------------
38
- @uri: "/grid5000/sites"
39
- ------------ PROPERTIES ------------
40
- "lille" => Restfully::Resource
41
- "grenoble" => Restfully::Resource
42
- "toulouse" => Restfully::Resource
43
- "sophia" => Restfully::Resource
44
- "rennes" => Restfully::Resource
45
- "lyon" => Restfully::Resource
46
- "nancy" => Restfully::Resource
47
- "bordeaux" => Restfully::Resource
48
- "orsay" => Restfully::Resource>
49
- irb(main):008:0> root['version']
50
- => "ee1f8111c8835496a9e4a6f7c9491c0238ee9827"
37
+ => #<Restfully::Collection:0x881f6c
38
+ ------------ META ------------
39
+ @uri: "/grid5000/sites"
40
+ @offset: 0
41
+ @total: 9
42
+ @version: Restfully::Resource
43
+ @versions: Restfully::Collection
44
+ ------------ PROPERTIES ------------
45
+ "version" => "4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8"
46
+ ------------ ITEMS ------------
47
+ Restfully::Resource
48
+ Restfully::Resource
49
+ Restfully::Resource
50
+ Restfully::Resource
51
+ Restfully::Resource
52
+ Restfully::Resource
53
+ Restfully::Resource
54
+ Restfully::Resource
55
+ Restfully::Resource>
56
+
57
+ irb(main):008:0> root.sites.by_uid('rennes').clusters.by_uid('paradent').nodes.by_uid('paradent-1').metrics.by_uid('mem_free', 'cpu_idle', 'bytes_in')
58
+ => [#<Restfully::Resource:0x4259a
59
+ ------------ META ------------
60
+ @uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/mem_free"
61
+ @uid: "mem_free"
62
+ @type: "metric"
63
+ @parent: Restfully::Resource
64
+ @timeseries: Restfully::Resource
65
+ ------------ PROPERTIES ------------
66
+ "last_update" => -901639018
67
+ "step" => 15
68
+ "timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>, #<Restfully::Resource:0x12444
69
+ ------------ META ------------
70
+ @uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/cpu_idle"
71
+ @uid: "cpu_idle"
72
+ @type: "metric"
73
+ @parent: Restfully::Resource
74
+ @timeseries: Restfully::Resource
75
+ ------------ PROPERTIES ------------
76
+ "last_update" => -901639018
77
+ "step" => 15
78
+ "timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>, #<Restfully::Resource:0xe4c870
79
+ ------------ META ------------
80
+ @uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/bytes_in"
81
+ @uid: "bytes_in"
82
+ @type: "metric"
83
+ @parent: Restfully::Resource
84
+ @timeseries: Restfully::Resource
85
+ ------------ PROPERTIES ------------
86
+ "last_update" => -901639018
87
+ "step" => 15
88
+ "timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>]
51
89
 
52
90
  irb(main):009:0> root.version
53
91
  => #<Restfully::Resource:0x8facf0
@@ -90,4 +128,4 @@ See the +examples+ directory for examples.
90
128
 
91
129
  == Copyright
92
130
 
93
- Copyright (c) 2009 Cyril Rohr. See LICENSE for details.
131
+ Copyright (c) 2009 Cyril Rohr, INRIA Rennes - Bretagne Atlantique. See LICENSE for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.3.0
@@ -9,26 +9,31 @@ require 'logger'
9
9
  require 'pp'
10
10
  require 'yaml'
11
11
 
12
- @options = {}
12
+
13
+ logger = Logger.new(STDOUT)
14
+ logger.level = Logger::WARN
15
+ @options = {:logger => logger}
13
16
  option_parser = OptionParser.new do |opts|
14
17
  opts.banner = "Usage: restfully base_uri [root_path] [options]"
15
18
 
16
19
  opts.on("-u=", "--username=", "Sets the username") do |u|
17
- @options['username'] = u
20
+ @options[:username] = u
18
21
  end
19
22
  opts.on("-p=", "--password=", "Sets the user password") do |p|
20
- @options['password'] = p
23
+ @options[:password] = p
21
24
  end
22
25
  opts.on("-c=", "--config=", "Sets the various options based on a custom YAML configuration file") do |v|
23
- @options['configuration_file'] = v
24
- end
25
- opts.on("-v", "--verbose", "Run verbosely") do |v|
26
- @options['verbose'] = v
26
+ @options[:configuration_file] = v
27
27
  end
28
28
  opts.on("--log=", "Outputs log messages to the given file. Defaults to stdout") do |v|
29
- @options['log'] = v
29
+ original_logger_level = logger.level
30
+ logger = Logger.new(File.expand_path(v))
31
+ logger.level = original_logger_level
32
+ @options[:logger] = logger
33
+ end
34
+ opts.on("-v", "--verbose", "Run verbosely") do |v|
35
+ @options[:logger].level = Logger::DEBUG
30
36
  end
31
-
32
37
  opts.on_tail("-h", "--help", "Show this message") do
33
38
  puts opts
34
39
  exit
@@ -38,44 +43,61 @@ end
38
43
 
39
44
  option_parser.parse!
40
45
 
41
- @base_uri = ARGV.shift
42
- @root_path = ARGV.shift || "/"
46
+ @options[:base_uri] = ARGV.shift
47
+ @options[:root_path] = ARGV.shift || "/"
43
48
 
44
- if (config_filename = @options.delete('configuration_file')) && File.exists?(File.expand_path(config_filename))
45
- config = YAML.load_file(File.expand_path(config_filename))
46
- @base_uri = config.delete('base_uri') || @base_uri
47
- @root_path = config.delete('root_path') || @root_path
48
- @options.merge!(config)
49
+ def session
50
+ @session ||= Restfully::Session.new(@options)
49
51
  end
50
52
 
51
- unless @base_uri
52
- $stderr.puts option_parser.help
53
- exit(-1)
54
- else
55
- if (log_file=@options.delete('log'))
56
- @logger = Logger.new(File.expand_path(log_file))
57
- else
58
- @logger = Logger.new(STDOUT)
59
- end
60
- if @options.delete('verbose')
61
- @logger.level = Logger::DEBUG
62
- else
63
- @logger.level = Logger::WARN
64
- end
65
-
66
- def session
67
- @session ||= Restfully::Session.new(@base_uri, @options.merge('root_path' => @root_path, 'logger' => @logger))
68
- end
69
- def root
70
- @root ||= Restfully::Resource.new(session.root_path, session).load
71
- end
72
-
73
- root # preloads
74
-
75
- require 'irb'
76
- require 'irb/completion'
77
-
78
- ARGV.clear
79
- IRB.start
80
- exit!
53
+ def root
54
+ @root ||= session.root.load
55
+ rescue Restfully::HTTP::Error => e
56
+ puts "#{e.class.name}: #{e.message}"
81
57
  end
58
+
59
+ require 'irb'
60
+ require 'irb/completion'
61
+ ARGV.clear
62
+ IRB.start
63
+ exit!
64
+
65
+
66
+ # if (config_filename = @options.delete('configuration_file')) && File.exists?(File.expand_path(config_filename))
67
+ # config = YAML.load_file(File.expand_path(config_filename))
68
+ # @base_uri = config.delete('base_uri') || @base_uri
69
+ # @root_path = config.delete('root_path') || @root_path
70
+ # @options.merge!(config)
71
+ # end
72
+ #
73
+ # unless @base_uri
74
+ # $stderr.puts option_parser.help
75
+ # exit(-1)
76
+ # else
77
+ # if (log_file=@options.delete('log'))
78
+ # @logger = Logger.new(File.expand_path(log_file))
79
+ # else
80
+ # @logger = Logger.new(STDOUT)
81
+ # end
82
+ # if @options.delete('verbose')
83
+ # @logger.level = Logger::DEBUG
84
+ # else
85
+ # @logger.level = Logger::WARN
86
+ # end
87
+ #
88
+ # def session
89
+ # @session ||= Restfully::Session.new(@base_uri, @options.merge('root_path' => @root_path, 'logger' => @logger))
90
+ # end
91
+ # def root
92
+ # @root ||= Restfully::Resource.new(session.root_path, session).load
93
+ # end
94
+ #
95
+ # root # preloads
96
+ #
97
+ # require 'irb'
98
+ # require 'irb/completion'
99
+ #
100
+ # ARGV.clear
101
+ # IRB.start
102
+ # exit!
103
+ # end
@@ -10,19 +10,19 @@ logger.level = Logger::INFO
10
10
  # Restfully.adapter = Restfully::HTTP::RestClientAdapter
11
11
  # Restfully.adapter = Patron::Session
12
12
  RestClient.log = 'stdout'
13
- Restfully::Session.new('https://localhost:3443/sid', 'root_path' => '/grid5000', 'logger' => logger) do |grid, session|
13
+ Restfully::Session.new(:base_uri => 'http://api.local/sid', :root_path => '/grid5000', :logger => logger) do |grid, session|
14
14
  grid_stats = {'hardware' => {}, 'system' => {}}
15
- grid.sites.each do |site_uid, site|
16
- site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, (node_uid, node_status)|
15
+ grid.sites.each do |site|
16
+ site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, node_status|
17
17
  accu['hardware'][node_status['hardware_state']] = (accu['hardware'][node_status['hardware_state']] || 0) + 1
18
18
  accu['system'][node_status['system_state']] = (accu['system'][node_status['system_state']] || 0) + 1
19
19
  accu
20
- }
20
+ } rescue {'hardware' => {}, 'system' => {}}
21
21
  grid_stats['hardware'].merge!(site_stats['hardware']) { |key,oldval,newval| oldval+newval }
22
22
  grid_stats['system'].merge!(site_stats['system']) { |key,oldval,newval| oldval+newval }
23
- p [site_uid, site_stats]
23
+ p [site.uid, site_stats]
24
24
  end
25
25
  p [:total, grid_stats]
26
26
  puts "Getting status of a few nodes in rennes:"
27
- pp grid.sites['rennes'].status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
27
+ pp grid.sites.by_uid('rennes').status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
28
28
  end
@@ -11,7 +11,8 @@ require 'restfully/resource'
11
11
  require 'restfully/collection'
12
12
 
13
13
 
14
- module Restfully
14
+ module Restfully
15
+ VERSION = "0.3.0"
15
16
  class << self
16
17
  attr_accessor :adapter
17
18
  end
@@ -1,41 +1,87 @@
1
1
  require 'delegate'
2
2
 
3
3
  module Restfully
4
- class Collection < DelegateClass(Hash)
4
+ # Resource and Collection classes should clearly include a common module to deal with links, attributes, associations and loading
5
+ class Collection < DelegateClass(Array)
5
6
 
6
- attr_reader :state, :raw, :uri, :session, :title
7
+ attr_reader :state, :raw, :uri, :session, :title, :offset, :total
7
8
 
8
9
  def initialize(uri, session, options = {})
9
10
  options = options.symbolize_keys
10
11
  @uri = uri
11
12
  @title = options[:title]
12
13
  @session = session
13
- @raw = options[:raw]
14
14
  @state = :unloaded
15
- @items = {}
15
+ @items = []
16
+ @indexes = {}
17
+ @associations = {}
18
+ @attributes = {}
16
19
  super(@items)
17
20
  end
18
21
 
19
22
  def loaded?; @state == :loaded; end
20
23
 
24
+ def method_missing(method, *args)
25
+ load
26
+ if association = @associations[method.to_s]
27
+ session.logger.debug "Loading association #{method}, args=#{args.inspect}"
28
+ association.load(*args)
29
+ elsif method.to_s =~ /^by_(.+)$/
30
+ key = $1
31
+ @indexes[method.to_s] ||= @items.inject({}) { |accu, item|
32
+ accu[item.has_key?(key) ? item[key] : item.send(key.to_sym)] = item
33
+ accu
34
+ }
35
+ if args.empty?
36
+ @indexes[method.to_s]
37
+ elsif args.length == 1
38
+ @indexes[method.to_s][args.first]
39
+ else
40
+ @indexes[method.to_s].values_at(*args)
41
+ end
42
+ else
43
+ super(method, *args)
44
+ end
45
+ end
46
+
21
47
  def load(options = {})
22
48
  options = options.symbolize_keys
23
49
  force_reload = !!options.delete(:reload) || options.has_key?(:query)
24
- if loaded? && !force_reload
50
+ if loaded? && !force_reload && options[:raw].nil?
25
51
  self
26
- else
52
+ else
53
+ @raw = options[:raw]
54
+ @associations.clear
55
+ @attributes.clear
27
56
  @items.clear
28
57
  if raw.nil? || force_reload
29
58
  response = session.get(uri, options)
30
59
  @raw = response.body
31
60
  end
32
61
  raw.each do |key, value|
33
- next if key == 'links'
34
- self_link = (value['links'] || []).map{|link| Link.new(link)}.detect{|link| link.self?}
35
- if self_link && self_link.valid?
36
- @items.store(key, Resource.new(self_link.href, session, :raw => value).load)
62
+ case key
63
+ when "total", "offset"
64
+ instance_variable_set("@#{key}".to_sym, value)
65
+ when "links"
66
+ value.each{|link| define_link(Link.new(link))}
67
+ when "items"
68
+ value.each do |item|
69
+ self_link = (item['links'] || []).map{|link| Link.new(link)}.detect{|link| link.self?}
70
+ if self_link && self_link.valid?
71
+ @items << Resource.new(self_link.href, session).load(:raw => item)
72
+ else
73
+ session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
74
+ end
75
+ end
37
76
  else
38
- session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
77
+ case value
78
+ when Hash
79
+ @attributes.store(key, SpecialHash.new.replace(value)) unless @associations.has_key?(key)
80
+ when Array
81
+ @attributes.store(key, SpecialArray.new(value))
82
+ else
83
+ @attributes.store(key, value)
84
+ end
39
85
  end
40
86
  end
41
87
  @state = :loaded
@@ -43,21 +89,58 @@ module Restfully
43
89
  end
44
90
  end
45
91
 
92
+ def respond_to?(method, *args)
93
+ @associations.has_key?(method.to_s) || super(method, *args)
94
+ end
95
+
46
96
  def inspect(options = {:space => "\t"})
47
97
  output = "#<#{self.class}:0x#{self.object_id.to_s(16)}"
48
98
  if loaded?
49
99
  output += "\n#{options[:space]}------------ META ------------"
50
100
  output += "\n#{options[:space]}@uri: #{uri.inspect}"
51
- unless @items.empty?
101
+ output += "\n#{options[:space]}@offset: #{offset.inspect}"
102
+ output += "\n#{options[:space]}@total: #{total.inspect}"
103
+ @associations.each do |title, assoc|
104
+ output += "\n#{options[:space]}@#{title}: #{assoc.class.name}"
105
+ end
106
+ unless @attributes.empty?
52
107
  output += "\n#{options[:space]}------------ PROPERTIES ------------"
53
- @items.each do |key, value|
54
- output += "\n#{options[:space]}#{key.inspect} => #{value.class.name}"
108
+ @attributes.each do |key, value|
109
+ output += "\n#{options[:space]}#{key.inspect} => #{value.inspect}"
110
+ end
111
+ end
112
+ unless @items.empty?
113
+ output += "\n#{options[:space]}------------ ITEMS ------------"
114
+ @items.each do |value|
115
+ output += "\n#{options[:space]}#{value.class.name}"
55
116
  end
56
117
  end
57
118
  end
58
119
  output += ">"
59
120
  end
60
121
 
61
-
122
+ protected
123
+ def define_link(link)
124
+ if link.valid?
125
+ case link.rel
126
+ when 'parent'
127
+ @associations['parent'] = Resource.new(link.href, session)
128
+ when 'collection'
129
+ raw_included = link.resolved? ? raw[link.title] : nil
130
+ @associations[link.title] = Collection.new(link.href, session,
131
+ :raw => raw_included,
132
+ :title => link.title)
133
+ when 'member'
134
+ raw_included = link.resolved? ? raw[link.title] : nil
135
+ @associations[link.title] = Resource.new(link.href, session,
136
+ :title => link.title,
137
+ :raw => raw_included)
138
+ when 'self'
139
+ # we do nothing
140
+ end
141
+ else
142
+ session.logger.warn link.errors.join("\n")
143
+ end
144
+ end
62
145
  end
63
146
  end