socket_common: limit IO#write garbage workaround to <= 2.4
[ruby-mogilefs-client.git] / lib / mogilefs / admin.rb
blob96a7898c0d91c467f0ee74bab72e32658813b87e
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"=>7500,
33   #     "hostid"=>1,
34   #     "hostip"=>"192.168.1.2",
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   #     "utilization"=>0.0,
61   #     "reject_bad_md5"=>false,
62   #     "observed_state"=>"writeable",
63   #     "mb_total"=>666666}]
65   def get_devices(devid = nil)
66     to_i = %w(mb_asof mb_free mb_used mb_total devid weight hostid)
67     want = %w(status reject_bad_md5 observed_state utilization).concat(to_i)
68     rv = @backend.get_devices(devid ? { :devid => devid } : {})
69     rv = clean('devices', 'dev', rv, true, to_i, want)
71     rv.each do |row|
72       u = row["utilization"] and row["utilization"] = u.to_f
74       case row["observed_state"]
75       when ""
76         row["observed_state"] = nil
77       end
79       # be sure we do not set this at all for pre-2.60 MogileFS-Server
80       case row["reject_bad_md5"]
81       when "1"
82         row["reject_bad_md5"] = true
83       when "0"
84         row["reject_bad_md5"] = false
85       when ""
86         row["reject_bad_md5"] = nil
87       end
88     end
89   end
91   ##
92   # Returns an Array of fid Hashes from +from_fid+, limited to +count+
93   #
94   #   admin.list_fids 0, 100
95   #
96   # Returns:
97   #
98   #   [{"fid"=>99,
99   #     "class"=>"normal",
100   #     "domain"=>"test",
101   #     "devcount"=>2,
102   #     "length"=>4,
103   #     "key"=>"file_key"},
104   #    {"fid"=>82,
105   #     "class"=>"normal",
106   #     "devcount"=>2,
107   #     "domain"=>"test",
108   #     "length"=>9,
109   #     "key"=>"new_new_key"}]
111   def list_fids(from_fid, count = 100)
112     to_i = %w(fid devcount length)
113     want = %w(domain class key).concat(to_i)
114     rv = @backend.list_fids(:from => from_fid, :to => count)
115     # :to is now :count internally in mogilefsd
116     clean('fid_count', 'fid_', rv, true, to_i, want)
117   end
119   ##
120   # Returns a statistics structure representing the state of mogilefs.
121   #
122   # *** This command no longer works with recent versions of MogileFS ***
123   # *** Use mogstats(1) from the MogileFS::Utils package on CPAN ***
124   # *** We will remove this method in 4.x ***
125   #
126   #   admin.get_stats
127   #
128   # Returns:
129   #
130   #   {"fids"=>{"max"=>"99", "count"=>"2"},
131   #    "device"=>
132   #     [{"status"=>"alive", "files"=>"2", "id"=>"1", "host"=>"rur-1"},
133   #      {"status"=>"alive", "files"=>"2", "id"=>"2", "host"=>"rur-2"}],
134   #    "replication"=>
135   #     [{"files"=>"2", "class"=>"normal", "devcount"=>"2", "domain"=>"test"}],
136   #    "file"=>[{"files"=>"2", "class"=>"normal", "domain"=>"test"}]}
138   def get_stats(type = 'all')
139     res = @backend.stats type => 1
140     stats = {}
142     stats['device'] = clean 'devicescount', 'devices', res, false
143     stats['file'] = clean 'filescount', 'files', res, false
144     stats['replication'] = clean 'replicationcount', 'replication', res, false
146     if res['fidmax'] or res['fidcount'] then
147       stats['fids'] = {
148         'max' => res['fidmax'].to_i,
149         'count' => res['fidcount'].to_i
150       }
151     end
153     %w(device file replication).each do |s|
154       stats.delete(s) if stats[s].empty?
155     end
157     stats
158   end
160   ##
161   # Returns the domains and classes, and their policies present in the mogilefs.
162   #
163   #   admin.get_domains
164   #
165   # Returns (on newer MogileFS servers):
166   #   {
167   #     "test" => {
168   #       "default" => {
169   #         "mindevcount" => 2,
170   #         "replpolicy" => "MultipleHosts()",
171   #         "hashtype => nil,
172   #       }
173   #     }
174   #   }
175   #
176   # Returns (on older MogileFS servers without replication policies):
177   #
178   #   {"test"=>{"normal"=>3, "default"=>2}}
180   def get_domains
181     res = @backend.get_domains
182     have_replpolicy = false
184     domains = {}
185     to_i = %w(mindevcount)
186     want = %w(name replpolicy hashtype mindevcount)
187     (1..res['domains'].to_i).each do |i|
188       domain = clean("domain#{i}classes", "domain#{i}class", res, false, to_i,
189                      want)
190       tmp = domains[res["domain#{i}"]] = {}
191       domain.each do |d|
192         tmp[d.delete("name")] = d
193         have_replpolicy ||= d.include?("replpolicy")
194       end
195     end
197     # only for MogileFS 1.x?, maybe we can drop support for this...
198     unless have_replpolicy
199       domains.each do |namespace, class_data|
200         class_data.each do |class_name, data|
201           class_data[class_name] = data["mindevcount"]
202         end
203       end
204     end
206     domains
207   end
209   ##
210   # Creates a new domain named +domain+.  Returns nil if creation failed.
212   def create_domain(domain)
213     raise MogileFS::ReadOnlyError if readonly?
214     res = @backend.create_domain :domain => domain
215     res ? res['domain'] : nil
216   end
218   ##
219   # Deletes +domain+.  Returns true if successful, raises
220   # MogileFS::Backend::DomainNotFoundError if not
222   def delete_domain(domain)
223     raise MogileFS::ReadOnlyError if readonly?
224     ! @backend.delete_domain(:domain => domain).nil?
225   end
227   ##
228   # Creates a new class in +domain+ named +klass+ with +policy+ for
229   # replication.  Raises on failure.
231   def create_class(domain, klass, policy)
232     modify_class(domain, klass, policy, :create)
233   end
235   ##
236   # Updates class +klass+ in +domain+ with +policy+ for replication.
237   # Raises on failure.
239   def update_class(domain, klass, policy)
240     modify_class(domain, klass, policy, :update)
241   end
243   ##
244   # Removes class +klass+ from +domain+.  Returns true if successful.
245   # Raises on failure
247   def delete_class(domain, klass)
248     ! @backend.delete_class(:domain => domain, :class => klass).nil?
249   end
251   ##
252   # Creates a new host named +host+.  +args+ must contain :ip and :port.
253   # Returns true if successful, false if not.
255   def create_host(host, args = {})
256     raise ArgumentError, "Must specify ip and port" unless \
257       args.include? :ip and args.include? :port
259     modify_host(host, args, 'create')
260   end
262   ##
263   # Updates +host+ with +args+.  Returns true if successful, false if not.
265   def update_host(host, args = {})
266     modify_host(host, args, 'update')
267   end
269   ##
270   # Deletes host +host+.  Returns nil on failure.
272   def delete_host(host)
273     raise MogileFS::ReadOnlyError if readonly?
274     ! @backend.delete_host(:host => host).nil?
275   end
277   ##
278   # Creates device with Integer +devid+ on +host+
279   # +host+ may be an integer for hostid or String for hostname
280   def create_device(host, devid, opts = {})
281     raise MogileFS::ReadOnlyError if readonly?
282     opts = opts.dup
284     case host
285     when Integer
286       opts[:hostid] = host
287     when String
288       opts[:hostname] = host
289     else
290       raise ArgumentError, "host=#{host.inspect} is not a String or Integer"
291     end
293     opts[:devid] = devid
294     ! @backend.create_device(opts).nil?
295   end
297   ##
298   # Changes the device status of +device+ on +host+ to +state+ which can be
299   # 'alive', 'down', or 'dead'.
301   def change_device_state(host, device, state)
302     raise MogileFS::ReadOnlyError if readonly?
303     ! @backend.set_state(:host => host, :device => device, :state => state).nil?
304   end
306   ##
307   # Changes the device weight of +device+ on +host+ to +weight+.
308   # +weight+ should be a non-negative Integer.  Devices with higher
309   # +weight+ values are more likely to be chosen for reads and writes.
310   def change_device_weight(host, device, weight)
311     raise MogileFS::ReadOnlyError if readonly?
312     opts = { :host => host, :device => device, :weight => weight }
313     ! @backend.set_weight(opts).nil?
314   end
316   # reschedules all deferred replication, returns a hash with the number
317   # of files rescheduled:
318   #
319   #   admin.replicate_now => { "count" => 5 }
320   def replicate_now
321     rv = @backend.replicate_now
322     rv["count"] = rv["count"].to_i
323     rv
324   end
326   # Clears the tracker caches.  Not implemented in all versions of MogileFS
327   def clear_cache
328     @backend.clear_cache
329   end
331   protected unless defined? $TESTING
333   ##
334   # Modifies +klass+ on +domain+ to store files on +mindevcount+ devices via
335   # +action+.  Returns the class name if successful, raises if not
337   def modify_class(domain, klass, policy, action)
338     raise MogileFS::ReadOnlyError if readonly?
339     args = { :domain => domain, :class => klass }
340     case policy
341     when Integer
342       args[:mindevcount] = policy
343     when String
344       args[:replpolicy] = policy
345     when Hash
346       args.merge!(policy)
347     else
348       raise ArgumentError,
349            "policy=#{policy.inspect} not understood for #{action}_class"
350     end
351     @backend.__send__("#{action}_class", args)["class"]
352   end
354   ##
355   # Modifies +host+ using +args+ via +action+.  Returns true if successful,
356   # false if not.
358   def modify_host(host, args = {}, action = 'create')
359     args[:host] = host
360     ! @backend.__send__("#{action}_host", args).nil?
361   end
363   ##
364   # Turns the response +res+ from the backend into an Array of Hashes from 1
365   # to res[+count+].  If +underscore+ is true then a '_' character is assumed
366   # between the prefix and the hash key value.
367   #
368   #   res = {"host1_remoteroot"=>"/mnt/mogilefs/rur-1",
369   #          "host1_hostname"=>"rur-1",
370   #          "host1_hostid"=>"1",
371   #          "host1_http_get_port"=>"",
372   #          "host1_altip"=>"",
373   #          "hosts"=>"1",
374   #          "host1_hostip"=>"",
375   #          "host1_http_port"=>"",
376   #          "host1_status"=>"alive",
377   #          "host1_altmask"=>""}
378   #   admin.clean 'hosts', 'host', res
379   #
380   # Returns:
381   #
382   #   [{"status"=>"alive",
383   #     "http_get_port"=>nil,
384   #     "http_port"=>7600,
385   #     "hostid"=>1,
386   #     "hostip"=>"192.168.1.3",
387   #     "hostname"=>"rur-1",
388   #     "altip"=>"",
389   #     "altmask"=>""}]
391   def clean(count, prefix, res, underscore = true, to_i = [], want = nil)
392     empty = ""
393     underscore = underscore ? '_' : empty
395     # convert array to hash for O(1) lookups
396     to_i = to_i.inject({}) { |m,k| m[k] = m }
397     if want
398       (1..res[count].to_i).map do |i|
399         row = {}
400         want.each do |k|
401           v = res["#{prefix}#{i}#{underscore}#{k}"] or next
402           row[k] = to_i.include?(k) ? (empty == v ? nil : v.to_i) : v
403         end
404         row
405       end
406     else
407       keys = res.keys
408       (1..res[count].to_i).map do |i|
409         re = /^#{prefix}#{i}#{underscore}/
410         row = {}
411         keys.grep(re).each do |k|
412           v = res[k]
413           k = k.sub(re, empty)
414           row[k] = to_i.include?(k) ? (empty == v ? nil : v.to_i) : v
415         end
416         row
417       end
418     end
419   end