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