1b1676586bdd759ec2c5e434a7e8e73c2c6c07bb
[ruby-mogilefs-client.git] / lib / mogilefs / admin.rb
blob1b1676586bdd759ec2c5e434a7e8e73c2c6c07bb
1 # -*- encoding: binary -*-
3 # \MogileFS administration client, this has little real-world usage
4 # and is considered a work-in-progress
5 class MogileFS::Admin < MogileFS::Client
7   ##
8   # Enumerates fids using #list_fids.  Returns the number of valid fids
9   # processed
11   def each_fid
12     low = -1
13     rv = 0
14     begin
15       fids = list_fids(low)
16       fids.each { |fid| yield fid }
17       rv += fids.size
18     end while last = fids[-1] and low = last["fid"]
19     rv
20   end
22   ##
23   # Returns an Array of host status Hashes.  If +hostid+ is given only that
24   # host is returned.
25   #
26   #   admin.get_hosts 1
27   #
28   # Returns:
29   #
30   #   [{"status"=>"alive",
31   #     "http_get_port"=>nil,
32   #     "http_port"=>"",
33   #     "hostid"=>"1",
34   #     "hostip"=>"",
35   #     "hostname"=>"rur-1",
36   #     "altip"=>"",
37   #     "altmask"=>""}]
39   def get_hosts(hostid = nil)
40     to_i = %w(hostid http_port http_get_port)
41     want = %w(status hostip hostname altip altmask).concat(to_i)
42     rv = @backend.get_hosts(hostid ? { :hostid => hostid } : {})
43     clean('hosts', 'host', rv, true, to_i, want)
44   end
46   ##
47   # Returns an Array of device status Hashes.  If devid is given only that
48   # device is returned.
49   #
50   #   admin.get_devices 1
51   #
52   # Returns:
53   #
54   #   [{"status"=>"alive",
55   #     "mb_asof"=>nil,
56   #     "mb_free"=>666000,
57   #     "devid"=>1,
58   #     "hostid"=>1,
59   #     "mb_used"=>666,
60   #     "mb_total"=>666666}]
62   def get_devices(devid = nil)
63     to_i = %w(mb_asof mb_free mb_used mb_total devid weight hostid)
64     want = %w(status).concat(to_i)
65     rv = @backend.get_devices(devid ? { :devid => devid } : {})
66     rv = clean('devices', 'dev', rv, true, to_i, want)
67     rv.each do |row|
68       u = row["utilization"] and
69         row["utilization"] = nil == u ? nil : u.to_f
70       case row["reject_bad_md5"]
71       when "1"
72         row["reject_bad_md5"] = true
73       when "0"
74         row["reject_bad_md5"] = false
75       end
76     end
77   end
79   ##
80   # Returns an Array of fid Hashes from +from_fid+, limited to +count+
81   #
82   #   admin.list_fids 0, 100
83   #
84   # Returns:
85   #
86   #   [{"fid"=>99,
87   #     "class"=>"normal",
88   #     "domain"=>"test",
89   #     "devcount"=>2,
90   #     "length"=>4,
91   #     "key"=>"file_key"},
92   #    {"fid"=>82,
93   #     "class"=>"normal",
94   #     "devcount"=>2,
95   #     "domain"=>"test",
96   #     "length"=>9,
97   #     "key"=>"new_new_key"}]
99   def list_fids(from_fid, count = 100)
100     to_i = %w(fid devcount length)
101     want = %w(domain class key).concat(to_i)
102     rv = @backend.list_fids(:from => from_fid, :to => count)
103     # :to is now :count internally in mogilefsd
104     clean('fid_count', 'fid_', rv, true, to_i, want)
105   end
107   ##
108   # Returns a statistics structure representing the state of mogilefs.
109   #
110   # *** This command no longer works with recent versions of MogileFS ***
111   # *** Use mogstats(1) from the MogileFS::Utils package on CPAN ***
112   #
113   #   admin.get_stats
114   #
115   # Returns:
116   #
117   #   {"fids"=>{"max"=>"99", "count"=>"2"},
118   #    "device"=>
119   #     [{"status"=>"alive", "files"=>"2", "id"=>"1", "host"=>"rur-1"},
120   #      {"status"=>"alive", "files"=>"2", "id"=>"2", "host"=>"rur-2"}],
121   #    "replication"=>
122   #     [{"files"=>"2", "class"=>"normal", "devcount"=>"2", "domain"=>"test"}],
123   #    "file"=>[{"files"=>"2", "class"=>"normal", "domain"=>"test"}]}
125   def get_stats(type = 'all')
126     res = @backend.stats type => 1
127     stats = {}
129     stats['device'] = clean 'devicescount', 'devices', res, false
130     stats['file'] = clean 'filescount', 'files', res, false
131     stats['replication'] = clean 'replicationcount', 'replication', res, false
133     if res['fidmax'] or res['fidcount'] then
134       stats['fids'] = {
135         'max' => res['fidmax'].to_i,
136         'count' => res['fidcount'].to_i
137       }
138     end
140     stats.delete 'device' if stats['device'].empty?
141     stats.delete 'file' if stats['file'].empty?
142     stats.delete 'replication' if stats['replication'].empty?
144     stats
145   end
147   ##
148   # Returns the domains and classes, and their policies present in the mogilefs.
149   #
150   #   admin.get_domains
151   #
152   # Returns (on newer MogileFS servers):
153   #   {
154   #     "test" => {
155   #       "default" => {
156   #         "mindevcount" => 2,
157   #         "replpolicy" => "MultipleHosts()",
158   #         "hashtype => nil,
159   #       }
160   #     }
161   #   }
162   #
163   # Returns (on older MogileFS servers without replication policies):
164   #
165   #   {"test"=>{"normal"=>3, "default"=>2}}
167   def get_domains
168     res = @backend.get_domains
169     have_replpolicy = false
171     domains = {}
172     to_i = %w(mindevcount)
173     want = %w(name replpolicy hashtype mindevcount)
174     (1..res['domains'].to_i).each do |i|
175       domain = clean("domain#{i}classes", "domain#{i}class", res, false, to_i,
176                      want)
177       tmp = domains[res["domain#{i}"]] = {}
178       domain.each do |d|
179         tmp[d.delete("name")] = d
180         have_replpolicy ||= d.include?("replpolicy")
181       end
182     end
184     # only for MogileFS 1.x?, maybe we can drop support for this...
185     unless have_replpolicy
186       domains.each do |namespace, class_data|
187         class_data.each do |class_name, data|
188           class_data[class_name] = data["mindevcount"]
189         end
190       end
191     end
193     domains
194   end
196   ##
197   # Creates a new domain named +domain+.  Returns nil if creation failed.
199   def create_domain(domain)
200     raise MogileFS::ReadOnlyError if readonly?
201     res = @backend.create_domain :domain => domain
202     res ? res['domain'] : nil
203   end
205   ##
206   # Deletes +domain+.  Returns true if successful, raises
207   # MogileFS::Backend::DomainNotFoundError if not
209   def delete_domain(domain)
210     raise MogileFS::ReadOnlyError if readonly?
211     ! @backend.delete_domain(:domain => domain).nil?
212   end
214   ##
215   # Creates a new class in +domain+ named +klass+ with +policy+ for
216   # replication.  Raises on failure.
218   def create_class(domain, klass, policy)
219     modify_class(domain, klass, policy, :create)
220   end
222   ##
223   # Updates class +klass+ in +domain+ with +policy+ for replication.
224   # Raises on failure.
226   def update_class(domain, klass, policy)
227     modify_class(domain, klass, policy, :update)
228   end
230   ##
231   # Removes class +klass+ from +domain+.  Returns true if successful.
232   # Raises on failure
234   def delete_class(domain, klass)
235     ! @backend.delete_class(:domain => domain, :class => klass).nil?
236   end
238   ##
239   # Creates a new host named +host+.  +args+ must contain :ip and :port.
240   # Returns true if successful, false if not.
242   def create_host(host, args = {})
243     raise ArgumentError, "Must specify ip and port" unless \
244       args.include? :ip and args.include? :port
246     modify_host(host, args, 'create')
247   end
249   ##
250   # Updates +host+ with +args+.  Returns true if successful, false if not.
252   def update_host(host, args = {})
253     modify_host(host, args, 'update')
254   end
256   ##
257   # Deletes host +host+.  Returns nil on failure.
259   def delete_host(host)
260     raise MogileFS::ReadOnlyError if readonly?
261     ! @backend.delete_host(:host => host).nil?
262   end
264   ##
265   # Creates device with Integer +devid+ on +host+
266   # +host+ may be an integer for hostid or String for hostname
267   def create_device(host, devid, opts = {})
268     raise MogileFS::ReadOnlyError if readonly?
269     opts = opts.dup
271     case host
272     when Integer
273       opts[:hostid] = host
274     when String
275       opts[:hostname] = host
276     else
277       raise ArgumentError, "host=#{host.inspect} is not a String or Integer"
278     end
280     opts[:devid] = devid
281     ! @backend.create_device(opts).nil?
282   end
284   ##
285   # Changes the device status of +device+ on +host+ to +state+ which can be
286   # 'alive', 'down', or 'dead'.
288   def change_device_state(host, device, state)
289     raise MogileFS::ReadOnlyError if readonly?
290     ! @backend.set_state(:host => host, :device => device, :state => state).nil?
291   end
293   ##
294   # Changes the device weight of +device+ on +host+ to +weight+.
295   # +weight+ should be a non-negative Integer.  Devices with higher
296   # +weight+ values are more likely to be chosen for reads and writes.
297   def change_device_weight(host, device, weight)
298     raise MogileFS::ReadOnlyError if readonly?
299     opts = { :host => host, :device => device, :weight => weight }
300     ! @backend.set_weight(opts).nil?
301   end
303   # reschedules all deferred replication, returns a hash with the number
304   # of files rescheduled:
305   #
306   #   admin.replicate_now => { "count" => 5 }
307   def replicate_now
308     rv = @backend.replicate_now
309     rv["count"] = rv["count"].to_i
310     rv
311   end
313   # Clears the tracker caches.  Not implemented in all versions of MogileFS
314   def clear_cache
315     @backend.clear_cache
316   end
318   protected unless defined? $TESTING
320   ##
321   # Modifies +klass+ on +domain+ to store files on +mindevcount+ devices via
322   # +action+.  Returns the class name if successful, raises if not
324   def modify_class(domain, klass, policy, action)
325     raise MogileFS::ReadOnlyError if readonly?
326     args = { :domain => domain, :class => klass }
327     case policy
328     when Integer
329       args[:mindevcount] = policy
330     when String
331       args[:replpolicy] = policy
332     when Hash
333       args.merge!(policy)
334     else
335       raise ArgumentError,
336            "policy=#{policy.inspect} not understood for #{action}_class"
337     end
338     @backend.__send__("#{action}_class", args)["class"]
339   end
341   ##
342   # Modifies +host+ using +args+ via +action+.  Returns true if successful,
343   # false if not.
345   def modify_host(host, args = {}, action = 'create')
346     args[:host] = host
347     ! @backend.__send__("#{action}_host", args).nil?
348   end
350   ##
351   # Turns the response +res+ from the backend into an Array of Hashes from 1
352   # to res[+count+].  If +underscore+ is true then a '_' character is assumed
353   # between the prefix and the hash key value.
354   #
355   #   res = {"host1_remoteroot"=>"/mnt/mogilefs/rur-1",
356   #          "host1_hostname"=>"rur-1",
357   #          "host1_hostid"=>"1",
358   #          "host1_http_get_port"=>"",
359   #          "host1_altip"=>"",
360   #          "hosts"=>"1",
361   #          "host1_hostip"=>"",
362   #          "host1_http_port"=>"",
363   #          "host1_status"=>"alive",
364   #          "host1_altmask"=>""}
365   #   admin.clean 'hosts', 'host', res
366   #
367   # Returns:
368   #
369   #   [{"status"=>"alive",
370   #     "http_get_port"=>nil,
371   #     "http_port"=>7600,
372   #     "hostid"=>1,
373   #     "hostip"=>"192.168.1.3",
374   #     "hostname"=>"rur-1",
375   #     "altip"=>"",
376   #     "altmask"=>""}]
378   def clean(count, prefix, res, underscore = true, to_i = [], want = nil)
379     empty = ""
380     underscore = underscore ? '_' : empty
382     # convert array to hash for O(1) lookups
383     to_i = to_i.inject({}) { |m,k| m[k] = m }
384     if want
385       (1..res[count].to_i).map do |i|
386         row = {}
387         want.each do |k|
388           v = res["#{prefix}#{i}#{underscore}#{k}"] or next
389           row[k] = to_i.include?(k) ? (empty == v ? nil : v.to_i) : v
390         end
391         row
392       end
393     else
394       keys = res.keys
395       (1..res[count].to_i).map do |i|
396         re = /^#{prefix}#{i}#{underscore}/
397         row = {}
398         keys.grep(re).each do |k|
399           v = res[k]
400           k = k.sub(re, empty)
401           row[k] = to_i.include?(k) ? (empty == v ? nil : v.to_i) : v
402         end
403         row
404       end
405     end
406   end