ganeti_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,75 @@
1
+ == License
2
+
3
+ This Ruby Ganeti Client is release under AGPL licence (http://www.gnu.org/licenses/agpl-3.0.html)
4
+
5
+
6
+ == Todo
7
+
8
+ 1. Manually test the following methods + fix when bugs found
9
+ 1.1. redistribute_config
10
+ 1.2. instance_create
11
+ 1.3. instance_delete
12
+ 1.4. instance_reboot
13
+ 1.5. instance_shutdown
14
+ 1.6. instance_startup
15
+ 1.7. instance_reinstall
16
+ 1.8. instance_replace_disks
17
+ 1.9. instance_activate_disks
18
+ 1.10. instance_create_tags
19
+ 1.11. instance_delete_tags
20
+ 1.12. job_get
21
+ 1.13. job_delete
22
+ 1.14. node_evaluate
23
+ 1.15. node_migrate
24
+ 1.16. node_change_role
25
+ 1.17. node_get_storage
26
+ 1.18. node_modify_storage
27
+ 1.19. node_repair_storage
28
+ 1.20. node_get_tags
29
+ 1.21. node_create_tags
30
+ 1.22. node_delete_tags
31
+ 1.22. tags_create
32
+ 1.23. tags_delete
33
+ 2. Add some error handling
34
+ 3. Make some code improvements
35
+ 4. Add better comments
36
+ 5. Improve this README with better docs
37
+ 6. Write tests!
38
+
39
+
40
+ == About
41
+
42
+
43
+ == Installation
44
+
45
+ gem install ganeti_client
46
+
47
+
48
+ == Usage
49
+
50
+ # the last parameter is a boolean (true|false) to indicate if you want to display the response.
51
+ # this might be handy for debugging purposes
52
+ client = GanetiClient::Client.new("your-host-and-port", "username", "password", boolean)
53
+
54
+ # now you should be able to access the api resources by using the client instance.
55
+ # example:
56
+ info = client.get_info
57
+ => #<GanetiInfo:0x10151bb78>
58
+
59
+ # most methods return an object. When you use .to_json on an object, you get the json object returned
60
+ # then you can see all the attributes available
61
+ info.name
62
+ => "hostname"
63
+
64
+ == Contributing
65
+
66
+ 1. Fork the project
67
+ 2. Add your changes
68
+ 3. Write tests
69
+ 4. Send a pull request
70
+
71
+
72
+
73
+
74
+
75
+
@@ -0,0 +1,14 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'base64'
5
+ require 'json'
6
+
7
+ require 'ganeti_client/client'
8
+ require 'ganeti_client/ganeti_object'
9
+
10
+
11
+ module GanetiClient
12
+
13
+
14
+ end
@@ -0,0 +1,562 @@
1
+ module GanetiClient
2
+ class Client
3
+
4
+ attr_accessor :conn, :headers, :version, :show_response
5
+
6
+ def initialize(host, username, password, show_response = false)
7
+ self.show_output = show_output
8
+ uri = URI.parse(host)
9
+
10
+ self.conn = Net::HTTP.new(uri.host, uri.port)
11
+ self.conn.use_ssl = (uri.scheme == "http")? false : true
12
+
13
+ self.headers = authenticate(username, password)
14
+
15
+ self.version = self.get_version
16
+ end
17
+
18
+
19
+ # Cluster information resource
20
+ # Returns cluster information
21
+ def info_get
22
+ url = get_url("info")
23
+ response_body = JSON.parse(send_request("GET", url))
24
+
25
+ create_class("GanetiInfo")
26
+
27
+ return GanetiInfo.new(response_body)
28
+ end
29
+
30
+ # Redistrite configuration to all nodes
31
+ # Redistribute configuration to all nodes. The result will be a job id.
32
+ def redistribute_config
33
+ url = get_url("redistribute-config")
34
+ response_body = send_request("PUT", url)
35
+
36
+ return response_body
37
+ end
38
+
39
+ # The instance resource
40
+ # Returns a list of all available instances
41
+ # If the optional bool bulk argument is provided and set to a true value (i.e ?bulk=1), the output contains detailed information about instances as a list.
42
+ def instances_get(bulk = 0)
43
+ url = get_url("instances", {"bulk" => bulk})
44
+ response_body = JSON.parse(send_request("GET", url))
45
+
46
+ create_class("GanetiInstance")
47
+
48
+ list = Array.new
49
+ response_body.each { |item| list << GanetiInstance.new(item) }
50
+
51
+ return list
52
+ end
53
+
54
+ # Creates an instance
55
+ # If the options bool dry-run argument is provided, the job will not be actually executed, only the pre-execution checks will be done.
56
+ # Query-ing the job result will return, in boty dry-run and normal case, the list of nodes selected for the instance
57
+ # Returns: a job ID that can be used later for polling
58
+ #
59
+ # Example of instance info:
60
+ #
61
+ def instance_create(info, dry_run = 0)
62
+ url = get_url("instances", {"dry-run" => dry_run})
63
+ body = JSON.generate(info)
64
+ response_body = send_request("POST", url, body)
65
+
66
+ return response_body
67
+ end
68
+
69
+ # Instance-specific resource
70
+ # Returns information about an instance, similar to the bulk output from the instance list
71
+ def instance_get(name)
72
+ url = get_url("instances/#{name}")
73
+ response_body = JSON.parse(send_request("GET", url))
74
+
75
+ create_class("GanetiInstance")
76
+
77
+ return GanetiInstance.new(response_body)
78
+ end
79
+
80
+ # Instance-specific resource
81
+ # Deletes an instance
82
+ # It supports the dry-run argument
83
+ def instance_delete(name, dry_run)
84
+ url = get_url("instances/#{name}", {"dry-run" => dry_run})
85
+ response_body = send_request("DELETE", url)
86
+
87
+ return response_body
88
+ end
89
+
90
+ # Requests detailed information about an instance.
91
+ # An optional parameter, static (bool), can be set to return only static information from the configuration without querying the instance's nodes
92
+ # The result will be a job id
93
+ def instance_get_info(name, static = 0)
94
+ url = get_url("instances/#{name}/info", {"static" => static})
95
+ response_body = send_request("GET", url)
96
+
97
+ return response_body
98
+ end
99
+
100
+ # Reboots URI for an instance
101
+ # Reboots the instance
102
+ # The URI takes optional type=soft|hard|full and ignore_secondaries=0|1 parameters
103
+ #
104
+ # type defines the reboot type.
105
+ # soft is just a normal reboot, without terminating the hypervisor.
106
+ # hard means full shutdown (including terminating the hypervisor process) and startup again
107
+ # full is like hard but also recreates the configuration from ground up as if you would have don a gnt-instance shutdown and gnt-instance start on it
108
+ #
109
+ # it supports the dry-run argument
110
+ def instance_reboot(name, type = "soft", ignore_secondaries = 0, dry_run = 0)
111
+ url = get_url("instances/#{name}/reboot", {"type" => type, "ignore_secondaries" => ignore_secondaries, "dry_run" => 0})
112
+ response_body = JSON.parse(send_request("POST", url))
113
+
114
+ create_class("GanetiInstanceReboot")
115
+
116
+ return GanetiIntanceReboot.new(response_body)
117
+ end
118
+
119
+
120
+ # Instance shutdown URI
121
+ # Shutdowns an instance
122
+ # It supports the dry-run argument
123
+ def instance_shutdown(name, dry_run = 0)
124
+ url = get_url("instances/#{name}/shutdown", {"dry-run" => dry_run})
125
+ response_body = JSON.parse(send_request("PUT", url))
126
+
127
+ create_class("GanetiInstanceShutdown")
128
+
129
+ return GanetiInstanceShutdown.new(response_body)
130
+ end
131
+
132
+ # Instance startup URI
133
+ # Startup an instance
134
+ # The URI takes an optional force=1|0 parameter to start the instance even if secondary disks are failing
135
+ # It supports the dry-run argument
136
+ def instance_startup(name, force = 0, dry_run=0)
137
+ url = get_url("instances/#{name}/startup", {"force" => force, "dry-run" => dry_run})
138
+ body = "" # force parameter
139
+ response_body = send_request("PUT", url, body)
140
+
141
+ create_class("GanetiInstanceStartup")
142
+
143
+ return GanetiInstanceStartup.new(response_body)
144
+ end
145
+
146
+ # Install the operating system again
147
+ # Takes the parameters os (OS template name) and nostartup (bool)
148
+ def instance_reinstall(name, os_name, nostartup)
149
+ url = get_url("instances/#{name}/reinstall", {"os" => os_name, "nostartup" => nostartup})
150
+ response_body = send_request("POST", url)
151
+
152
+ create_class("GanetiInstanceReinstall")
153
+
154
+ return GanetiInstanceReinstall.new(response_body)
155
+ end
156
+
157
+ # Replaces disks on an instance
158
+ # Takes the parameters mode (one of replace_on_primary, replace_on_secondary or replace_auto), disks (comma seperated list of disk indexes), remote_node and iallocator
159
+ # Either remote_node or iallocator needs to be defined when using mode=replace_new_secondary
160
+ # mode is a mandatory parameter. replace_auto tries to determine the broken disk(s) on its own and replacing it
161
+ def instance_replace_disks(name, mode = "replace_auto", iallocator = "", remote_node = "", disks = "")
162
+ url = get_url("instances/#{name}/replace-disks", {"mode" => mode, "iallocator" => iallocator, "remote_node" => remote_node, "disks" => disks})
163
+ response_body = send_request("POST", url)
164
+
165
+ return "?"
166
+ end
167
+
168
+ # Activate disks on an instance
169
+ # Takes the bool parameter ignore_size. When set ignore the recorded size (useful for forcing activation when recoreded size is wrong)
170
+ def intance_activate_disks(name, ignore_size = 0)
171
+ url = get_url("instances/#{name}/activate-disks", {"ignore_size" => ignore_size})
172
+ response_body = send_request("PUT", url)
173
+
174
+ return "?"
175
+ end
176
+
177
+ # Deactivate disks on an instance
178
+ # Takes no parameters
179
+ def instance_deactivate_disks(name)
180
+ url = get_url("instances/#{name}/deactivate-disks")
181
+ response_body = send_request("PUT", url)
182
+
183
+ return "?"
184
+ end
185
+
186
+ # Returns a list of tags
187
+ #
188
+ # Example:
189
+ # ["tag1", "tag2", "tag3"]
190
+ def instance_get_tags(name)
191
+ url = get_url("instances/#{name}/tags")
192
+ response_body = JSON.parse(send_request("GET", url))
193
+
194
+ return response_body
195
+ end
196
+
197
+ # Add a set of tags
198
+ # The request as a list of strings shoud be PUT tot this URI. The result will be a job id
199
+ # It supports the dry-run argument
200
+ def instance_create_tags(name, dry_run = 0)
201
+ url = get_url("instances/#{name}/tags", {"dry-run" => dry_run})
202
+ response_body = send_request("PUT", url)
203
+
204
+ return response_body
205
+ end
206
+
207
+ # Delete a tag
208
+ # In order to delete a set of tags, the DELETE request sould be addressed to URI LIKE:
209
+ # /tags?tag=[tag]&tag=[tag]
210
+ # It supports the dry-run argument
211
+ def instance_delete_tags(name, dry_run = 0)
212
+ url = get_url("instances/#{name}/tags", {"dry-run" => dry_run})
213
+ response_body = send_request("DELETE", url)
214
+
215
+ return "?"
216
+ end
217
+
218
+ # Returns a dictionary of jobs
219
+ # Returns: a dictionary with jobs id and uri
220
+ def jobs_get
221
+ url = get_url("jobs")
222
+ response_body = JSON.parse(send_request("GET", url))
223
+
224
+ create_class("GanetiJob")
225
+
226
+ list = Array.new
227
+ response_body.each { |item| list << GanetiJob.new(item) }
228
+
229
+ return list
230
+ end
231
+
232
+ # Individual job URI
233
+ # Return a job status
234
+ # Returns: a dictionary with job parameters
235
+ #
236
+ # The result includes:
237
+ # id: job ID as number
238
+ # status: current job status as a string
239
+ # ops: involved OpCodes as a list of dictionaries for each opcodes in the job
240
+ # opstatus: OpCodes status as a list
241
+ # opresult: OpCodes results as a list
242
+ #
243
+ # For a successful opcode, the opresult field corresponding to it will contain the raw result from its LogicalUnit. In case an opcode has failed, its element in the opresult list will be a list of two elements:
244
+ # first element the error type (the Ganeti internal error name)
245
+ # second element a list of either one or two elements:
246
+ # the first element is the textual error description
247
+ # the second element, if any, will hold an error classification
248
+ #
249
+ # The error classification is most useful for the OpPrereqError error type - these errors happen before the OpCode has started executing, so it’s possible to retry the
250
+ # OpCode without side effects. But whether it make sense to retry depends on the error classification:
251
+ #
252
+ # resolver_error
253
+ # Resolver errors. This usually means that a name doesn’t exist in DNS, so if it’s a case of slow DNS propagation the operation can be retried later.
254
+ #
255
+ # insufficient_resources
256
+ # Not enough resources (iallocator failure, disk space, memory, etc.). If the resources on the cluster increase, the operation might succeed.
257
+ #
258
+ # wrong_input
259
+ # Wrong arguments (at syntax level). The operation will not ever be accepted unless the arguments change.
260
+ #
261
+ # wrong_state
262
+ # Wrong entity state. For example, live migration has been requested for a down instance, or instance creation on an offline node. The operation can be retried once the resource has changed state.
263
+ #
264
+ # unknown_entity
265
+ # Entity not found. For example, information has been requested for an unknown instance.
266
+ #
267
+ # already_exists
268
+ # Entity already exists. For example, instance creation has been requested for an already-existing instance.
269
+ #
270
+ # resource_not_unique
271
+ # Resource not unique (e.g. MAC or IP duplication).
272
+ #
273
+ # internal_error
274
+ # Internal cluster error. For example, a node is unreachable but not set offline, or the ganeti node daemons are not working, etc. A gnt-cluster verify should be run.
275
+ #
276
+ # environment_error
277
+ # Environment error (e.g. node disk error). A gnt-cluster verify should be run.
278
+ #
279
+ # Note that in the above list, by entity we refer to a node or instance, while by a resource we refer to an instance’s disk, or NIC, etc.
280
+ def job_get(job_id)
281
+ url = get_url("jobs/#{job_id}")
282
+ response_body = JSON.parse(send_request("GET", url))
283
+
284
+ create_class("GanetiJob")
285
+
286
+ return GanetiJob.new(response_body)
287
+ end
288
+
289
+ # Cancel a not-yet-started job
290
+ def job_delete(job_id)
291
+ url = get_url("jobs/#{job_id}")
292
+ response = send_request("DELETE", url)
293
+
294
+ return "?"
295
+ end
296
+
297
+ # Nodes resource
298
+ # Returns a list of all nodes
299
+ # If the optional ‘bulk’ argument is provided and set to ‘true’ value (i.e ‘?bulk=1’).
300
+ #
301
+ # Returns detailed information about nodes as a list.
302
+ def nodes_get(bulk = 0)
303
+ url = get_url("nodes", {"bulk", bulk})
304
+ response_body = JSON.parse(send_request("GET", url))
305
+
306
+ create_class("GanetiNode")
307
+
308
+ list = Array.new
309
+ response_body.each { |item| list << GanetiNode.new(item) }
310
+
311
+ return list
312
+ end
313
+
314
+ # Returns information about a node
315
+ def node_get(name)
316
+ url = get_url("nodes/#{name}")
317
+ response_body = JSON.parse(send_request("GET", url))
318
+
319
+ create_class("GanetiNode")
320
+
321
+ return GanetiNode.new(response_body)
322
+ end
323
+
324
+ # Evacuates all secondary instances off a node.
325
+ # To evacuate a node, either one of the iallocator or remote_node parameters must be passed:
326
+ #
327
+ # Example:
328
+ # evacuate?iallocator=[iallocator]
329
+ # evacuate?remote_node=[nodeX.example.com]
330
+ def node_evauate(name, iallocator = "", remote_node = "")
331
+ url = get_url("nodes/#{name}/evacuate", {"iallocator" => iallocator, "remote_node" => remote_node})
332
+ response_body = send_request("POST", url)
333
+
334
+ return "?"
335
+ end
336
+
337
+ # Migrates all primary instances of a node
338
+ # No parameters are required, but the bool parameter live can be set to use live migration (if available)
339
+ #
340
+ # Example:
341
+ # migrate?live=[0|1]
342
+ def node_migrate(name, live = 0)
343
+ url = get_url("nodes/#{name}/migrate", {"live" => live})
344
+ response_body = send_request("POST", url)
345
+
346
+ return "?"
347
+ end
348
+
349
+ # Get the node role
350
+ # Returns the current node role
351
+ #
352
+ # Example:
353
+ # "master-candidate"
354
+ #
355
+ # The rol is always one of the following:
356
+ # drained
357
+ # master
358
+ # master-candidate
359
+ # offline
360
+ # regular
361
+ def node_get_role(name)
362
+ url = get_url("nodes/#{name}/role")
363
+ response_body = send_request("GET", url)
364
+
365
+ return response_body
366
+ end
367
+
368
+ # Change the node role
369
+ # the request is a string which shoud be PUT to this URI. The result will be a job id
370
+ # It supports the bool force argument
371
+ #
372
+ # The rol is always one of the following:
373
+ # drained
374
+ # master
375
+ # master-candidate
376
+ # offline
377
+ # regular
378
+ def node_change_role(name, role, force = 0)
379
+ url = get_url("nodes/#{name}/role", {"role" => role, "force" => foce})
380
+ response_body = send_request("PUT", url)
381
+
382
+ return response_body
383
+ end
384
+
385
+ # Manages storage units on the node
386
+ # Requests a list of storage units on a node. Requires the parameters storage_type (one of file, lvm-pv or lvm-vg) and output_fields. The result will be a job id, using which the result can be retrieved
387
+ def node_get_storage(name, storage_type = "", output_fields = "")
388
+ url = get_url("nodes/#{name}/storage", {"storage_type" => storage_type, "output_fields" => output_fields})
389
+ response_body = send_request("GET", url)
390
+
391
+ return response_body
392
+ end
393
+
394
+ # Modify storage units on the node
395
+ # Mofifies parameters of storage units on the node. Requires the parameters storage_type (one of file, lvm-pv or lvm-vg) and name (name of the storage unit).
396
+ # Parameters can be passed additionally. Currently only allocatable (bool) is supported.
397
+ #
398
+ # The result will be a job id.
399
+ def node_modify_storage(name, storage_type, allocatable = 0)
400
+ url = get_url("nodes/#{name}/storage/modify", {"storage_type" => storage_type, "allocatable" => allocatable})
401
+ response_body = send_request("PUT", url)
402
+
403
+ return response_body
404
+ end
405
+
406
+
407
+ # Repairs a storage unit on the node. Requires the parameters storage_type (currently only lvm-vg can be repaired) and name (name of the storage unit).
408
+ #
409
+ # The result will be a job id
410
+ def node_repair_storage(name, storage_type = "lvm-vg")
411
+ url = get_url("nodes/#{name}/storage/repair", {"storage_type" => storage_type})
412
+ reponse_body = send_request("PUT", url)
413
+
414
+ return response_body
415
+ end
416
+
417
+
418
+ # Manages per-node tags
419
+ # Returns a list of tags
420
+ #
421
+ # Example:
422
+ # ["tag1","tag2", "tag3"]
423
+ def node_get_tags(name)
424
+ url = get_url("nodes/#{name}/tags")
425
+ response_body = send_request("GET", url)
426
+
427
+ return response_body
428
+ end
429
+
430
+ # Add a set of tags
431
+ # The request as a list of strings should be PUT to this URI.
432
+ # It supports the dry-run argument
433
+ #
434
+ # The result will be a job id
435
+ def node_create_tags(name, tags, dry_run = 0)
436
+ url = get_url("nodes/#{name}/tags", {"tags" => tags, "dry-run" => dry_run})
437
+ response_body = send_request("PUT", url)
438
+
439
+ return response_body
440
+ end
441
+
442
+ # Deletes tags
443
+ # In order to delete a set of tags, the DELETE request should be addressed to URI like:
444
+ # /tags?tag=[tag]&tag=[tag]
445
+ #
446
+ # It supports the dry-run argument
447
+ def node_delete_tags(name, tags, dry_run = 0)
448
+ url = get_url("nodes/#{name}/tags", {"tags" => targs, "dry-run" => dry_run})
449
+ response_body = send_request("DELETE", url)
450
+
451
+ return response_body
452
+ end
453
+
454
+ # OS resource
455
+ # Returns a list of all OSes
456
+ #
457
+ # Can return error 500 in case of a problem. Since this is a costly operation for Ganeti 2.0, it is not recommented to execute it too often
458
+ #
459
+ # Example:
460
+ # ["debian-etch"]
461
+ def os_list_get
462
+ url = get_url("os")
463
+ response_body = JSON.parse(send_request("GET", url))
464
+
465
+ return response_body
466
+ end
467
+
468
+ # Manages cluster tags
469
+ # Returns the cluster tags
470
+ #
471
+ # Example:
472
+ # ["tag1", "tag2", "tag3"]
473
+ def tags_get
474
+ url = get_url("tags")
475
+ response_body = JSON.parse(send_request("GET", url))
476
+
477
+ return response_body
478
+ end
479
+
480
+ # Adds a set of tags
481
+ # The request as a list of strings should be PUT to this URI. The result will be a job id
482
+ #
483
+ # It supports the dry-run argument
484
+ def tags_create(tags, dry_run = 0)
485
+ url = get_url("tags", {"tags" => tags, "dry-run" => dry_run})
486
+ response_body = send_request("PUT", url)
487
+
488
+ return response_body
489
+ end
490
+
491
+ # Deletes tags
492
+ # In order to delete a set of tags, the DELETE request should be addressed to URI like:
493
+ # /tags?tag=[tag]&tag=[tag]
494
+ #
495
+ # It supports the dry-run argument
496
+ def tags_delete(tags, dry_run = 0)
497
+ url = get_url("tags", {"tags" => tags, "dry-run" => dry_run})
498
+ response_body = send_request("DELETE", url)
499
+
500
+ return response_body
501
+ end
502
+
503
+
504
+ # The version resource
505
+ # This resource should be used to determine the remote API version and to adapt client accordingly
506
+ # Returns the remote API version. Ganeti 1.2 returns 1 and Ganeti 2.0 returns 2
507
+ def version_get
508
+ url = get_url("version")
509
+ response_body = send_request("GET", url)
510
+
511
+ return response_body
512
+ end
513
+
514
+
515
+ private
516
+
517
+ def authenticate(username, password)
518
+ basic = Base64.b64encode("#{username}:#{password}")
519
+ headers = {'Authorization' => "Basic #{basic}"}
520
+
521
+ return headers
522
+ end
523
+
524
+ def get_url(path, params = nil)
525
+ param_string = ""
526
+
527
+ if params
528
+ params.each do |key, value|
529
+ if value.kind_of?(Array)
530
+ value.each do |svalue|
531
+ param_string += "#{key}=#{svalue}"
532
+ end
533
+ else
534
+ param_string += "#{key}=#{value}&"
535
+ end
536
+ end
537
+ end
538
+ return (self.version)? "/#{self.version}/#{path}?#{param_string}" : "/#{path}?#{param_string}"
539
+ end
540
+
541
+ def send_request(method, url, body = nil)
542
+ response = self.conn.send_request(method, url, body)
543
+
544
+ puts "Response #{response.code} #{response.message}: #{response.body}" if self.show_reponse
545
+ return response.body.strip
546
+ end
547
+
548
+ def create_class(class_name)
549
+ unless(class_exists?(class_name))
550
+ klass = Class.new GanetiClient::GanetiObject
551
+ Object.const_set(class_name, klass)
552
+ end
553
+ end
554
+
555
+ def class_exists?(class_name)
556
+ klass = Module.const_get(class_name)
557
+ return klass.is_a?(Class)
558
+ rescue NameError
559
+ return false
560
+ end
561
+ end
562
+ end
@@ -0,0 +1,16 @@
1
+ module GanetiClient
2
+ class GanetiObject
3
+
4
+ attr_accessor :json_object
5
+
6
+ def initialize(json = {})
7
+ self.json_object = json
8
+
9
+ json.each { |attr_name, attr_value| self.class.send(:define_method, attr_name.to_sym){ return attr_value } }
10
+ end
11
+
12
+ def to_json
13
+ return self.json_object
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ganeti_client
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - "Micha\xC3\xABl Rigart"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-08-15 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Google Ganeti RAPI client for Ruby
23
+ email: michael@netronix.be
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README
30
+ files:
31
+ - README
32
+ - lib/ganeti_client.rb
33
+ - lib/ganeti_client/client.rb
34
+ - lib/ganeti_client/ganeti_object.rb
35
+ has_rdoc: true
36
+ homepage: http://www.netronix.be
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --charset=UTF-8
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ hash: 3
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ requirements: []
63
+
64
+ rubyforge_project: nowarning
65
+ rubygems_version: 1.3.7
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Google Ganeti Client
69
+ test_files: []
70
+