icontrol 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +9 -0
- data/README.rdoc +26 -10
- data/lib/icontrol/base.rb +57 -1
- data/lib/icontrol/base/attributable.rb +0 -21
- data/lib/icontrol/base/mappings.rb +1 -0
- data/lib/icontrol/base/sequence.rb +9 -0
- data/lib/icontrol/base/struct.rb +5 -1
- data/lib/icontrol/base/xml.rb +231 -0
- data/spec/icontrol/local_lb/pool_spec.rb +15 -16
- data/spec/icontrol/local_lb/profile_http_class_spec.rb +157 -75
- data/spec/icontrol/local_lb/virtual_server_spec.rb +1268 -0
- metadata +9 -4
data/.yardopts
ADDED
data/README.rdoc
CHANGED
@@ -17,19 +17,35 @@ In order to configure you just have to set up the username, password and the ser
|
|
17
17
|
|
18
18
|
= Using the library
|
19
19
|
|
20
|
+
Bassically the library is the same as the one documented in the f5 devcentral but using ruby docs (http://devcentral.f5.com/wiki/default.aspx/iControl/APIReference.html). The main difference is that while the icontrol api does allow the bulk operation over a number of elements at the same time, in this case we limit it to just one at a time. So, for example, if you one to set the default pool for a virtual server, in the f5 doc, it says you have to pass 2 parameters
|
21
|
+
set_default_pool_name(
|
22
|
+
in String [] virtual_servers,
|
23
|
+
in String [] default_pools
|
24
|
+
);
|
25
|
+
So you can change several default pools at a time. On the contrary this library adopts a more object oriented design and only allows to work with one object at a time, i. e. and talking ruby, you don't have a set_default_pool_name class method but an instance one, so you have to first retreive the virtual server and later set its default pool. Something like this
|
26
|
+
IControl::LocalLB::VirtualServer.find("my_virtual_server).set_default_pool_name(:default_pool => "the default pool name")
|
27
|
+
if you want to do it in every virtual server then you do something like this.
|
28
|
+
IControl::LocalLB::VirtualServer.find(:all).each { |vs| vs.set_default_pool_name(:default_pool => "the default pool name") }
|
29
|
+
I think you get the point. Note that now the parameter name is :default_pool and not :default_pools, because of the semantics change.
|
30
|
+
|
20
31
|
== Virtual Servers
|
21
32
|
|
22
33
|
You can retreive, create, delete and modify virtual servers, for more information see IControl::LocalLB::VirtualServer. As an example of you what you can do:
|
23
34
|
|
24
35
|
=== Creating a virtual Server
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
36
|
+
IControl::LocalLB::VirtualServer.create(:definition => {
|
37
|
+
:address => "192.168.99.99",
|
38
|
+
:name => "test_virtual_server",
|
39
|
+
:port => "4",
|
40
|
+
:protocol => IControl::Common::ProtocolType::PROTOCOL_TCP
|
41
|
+
},
|
42
|
+
:wildmask => "255.255.255.255",
|
43
|
+
:resource => {
|
44
|
+
:type => IControl::LocalLB::VirtualServer::VirtualServerType::RESOURCE_TYPE_POOL,
|
45
|
+
:default_pool_name => ""
|
46
|
+
},
|
47
|
+
:profiles => [])
|
48
|
+
|
33
49
|
|
34
50
|
=== Obtaining an instance of a virtual server
|
35
51
|
|
@@ -39,7 +55,7 @@ You can retreive, create, delete and modify virtual servers, for more informatio
|
|
39
55
|
|
40
56
|
=== Changing its default pool
|
41
57
|
|
42
|
-
my_virtual_server.default_pool
|
58
|
+
my_virtual_server.set_default_pool_name(:default_pool => "my_pool_name")
|
43
59
|
|
44
60
|
=== Destroying it
|
45
61
|
|
@@ -56,5 +72,5 @@ You can retreive, create, delete and modify virtual servers, for more informatio
|
|
56
72
|
* Send me a pull request. Bonus points for topic branches.
|
57
73
|
|
58
74
|
== Copyright
|
59
|
-
|
75
|
+
This documentation is based on the original documentation procided by F5 Networks, Inc., Seattle, Washington
|
60
76
|
Copyright (c) 2010 Jose Fernandez (magec). See LICENSE for details.
|
data/lib/icontrol/base.rb
CHANGED
@@ -13,6 +13,7 @@ require 'base/sequence'
|
|
13
13
|
require 'base/predeclarations'
|
14
14
|
require 'base/exception'
|
15
15
|
require 'base/icontrol_overlay'
|
16
|
+
require 'base/xml'
|
16
17
|
|
17
18
|
Savon.log = false
|
18
19
|
HTTPI::Adapter.use = :net_http
|
@@ -51,6 +52,11 @@ module IControl
|
|
51
52
|
include IControl::Base::Attributable
|
52
53
|
|
53
54
|
class << self
|
55
|
+
|
56
|
+
attr_accessor :id_name
|
57
|
+
def set_id_name(name)
|
58
|
+
@id_name = name
|
59
|
+
end
|
54
60
|
|
55
61
|
def class_name
|
56
62
|
name.split("::").last
|
@@ -79,8 +85,49 @@ module IControl
|
|
79
85
|
return @client
|
80
86
|
end
|
81
87
|
|
88
|
+
def symbolize(data)
|
89
|
+
if data.is_a? Hash
|
90
|
+
out = data.inject({}) do |hash,(key,value)|
|
91
|
+
value = case value
|
92
|
+
when ::Hash then value["xsi:nil"] ? nil : symbolize(value)
|
93
|
+
when ::Array then value.map { |val| symbolize(val) rescue val }
|
94
|
+
when ::String then value
|
95
|
+
end
|
96
|
+
|
97
|
+
new_key = if Savon.strip_namespaces?
|
98
|
+
key.strip_namespace.snakecase.to_sym
|
99
|
+
else
|
100
|
+
key.snakecase
|
101
|
+
end
|
102
|
+
if hash[new_key] # key already exists, value should be added as an Array
|
103
|
+
hash[new_key] = [hash[new_key], value].flatten
|
104
|
+
result = hash
|
105
|
+
else
|
106
|
+
result = hash.merge new_key => value
|
107
|
+
end
|
108
|
+
result
|
109
|
+
end
|
110
|
+
if data.respond_to?(:empty_node?)
|
111
|
+
out.instance_eval{ @_empty_node = data.empty_node? }
|
112
|
+
end
|
113
|
+
return out
|
114
|
+
else
|
115
|
+
return data
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
82
120
|
# Generic type mapping
|
83
121
|
def map_response(response)
|
122
|
+
|
123
|
+
response = Crack::XML.parse(response).to_hash
|
124
|
+
|
125
|
+
envelope = response[response.keys.first] || {}
|
126
|
+
body_key = envelope.keys.find { |key| /.+:Body/ =~ key } rescue nil
|
127
|
+
response = body_key ? envelope[body_key] : {}
|
128
|
+
|
129
|
+
response = symbolize(response)
|
130
|
+
|
84
131
|
response_key = response.keys.first
|
85
132
|
if response_key
|
86
133
|
if response[response_key].has_key? :return
|
@@ -155,6 +202,8 @@ module IControl
|
|
155
202
|
|
156
203
|
|
157
204
|
def pluralize(string)
|
205
|
+
string = string.to_s.gsub(/ss$/,"sse")
|
206
|
+
return string if string[-1..-1] == "s"
|
158
207
|
return "#{string}s"
|
159
208
|
end
|
160
209
|
|
@@ -175,7 +224,7 @@ module IControl
|
|
175
224
|
block.call(soap) if block
|
176
225
|
request = soap.to_xml
|
177
226
|
end
|
178
|
-
return self.map_response(response.
|
227
|
+
return self.map_response(response.to_xml)
|
179
228
|
rescue Savon::HTTP::Error, Savon::SOAP::Fault => error
|
180
229
|
IControl::Base::ExceptionFactory.raise_from_xml(error.http.body)
|
181
230
|
end
|
@@ -186,6 +235,13 @@ module IControl
|
|
186
235
|
|
187
236
|
end
|
188
237
|
|
238
|
+
def initialize(attributes)
|
239
|
+
@attributes = {}
|
240
|
+
id = attributes.delete(self.class.id_name) if attributes && attributes[self.class.id_name]
|
241
|
+
@attributes[:id] ||= id
|
242
|
+
super(attributes)
|
243
|
+
end
|
244
|
+
|
189
245
|
def default_body
|
190
246
|
{ self.class.id_name.to_s => @attributes[:id] }
|
191
247
|
end
|
@@ -7,38 +7,17 @@ module IControl
|
|
7
7
|
def self.included(klass)
|
8
8
|
klass.class_eval do
|
9
9
|
include InstanceMethods
|
10
|
-
class << self;
|
11
|
-
attr_accessor :id_name
|
12
|
-
def set_id_name(name)
|
13
|
-
@id_name = name
|
14
|
-
end
|
15
|
-
end
|
16
10
|
end
|
17
11
|
end
|
18
12
|
|
19
13
|
module InstanceMethods # :nodoc:
|
20
|
-
|
21
|
-
def id
|
22
|
-
@attributes[:id]
|
23
|
-
end
|
24
14
|
|
25
15
|
def type
|
26
16
|
return @attributes[:type] || super
|
27
17
|
end
|
28
18
|
|
29
19
|
def initialize(attributes)
|
30
|
-
id = attributes.delete(self.class.id_name) if attributes && attributes[self.class.id_name]
|
31
20
|
@attributes = attributes || {}
|
32
|
-
@attributes[:id] ||= id
|
33
|
-
|
34
|
-
# Now we define an alias for the id_name
|
35
|
-
id_name = self.class.id_name
|
36
|
-
|
37
|
-
(class << self; self; end).instance_eval do
|
38
|
-
define_method(id_name) do
|
39
|
-
@attributes[:id]
|
40
|
-
end
|
41
|
-
end if id_name
|
42
21
|
end
|
43
22
|
|
44
23
|
def method_missing(method_name,*args,&block)
|
@@ -17,6 +17,7 @@ module IControl # :nodoc:
|
|
17
17
|
return String if type == "y:string"
|
18
18
|
return LongSequence if type == "y:longSequence"
|
19
19
|
return StringSequence if type == "y:stringSequence"
|
20
|
+
return BooleanSequence if type == "y:booleanSequence"
|
20
21
|
|
21
22
|
splitted = type.split(":")
|
22
23
|
temp_name = [splitted.shift,*splitted.shift.split(".")]
|
@@ -6,6 +6,7 @@ class BasicSequence
|
|
6
6
|
end
|
7
7
|
def from_soap(xml)
|
8
8
|
object = [*xml[:item]].map { |i| i.send(@conversion_method) }
|
9
|
+
return nil if object.empty?
|
9
10
|
return object.length == 1 ? object.first : object
|
10
11
|
end
|
11
12
|
end
|
@@ -31,6 +32,14 @@ class LongSequence < BasicSequence
|
|
31
32
|
set_conversion_method :to_i
|
32
33
|
end
|
33
34
|
|
35
|
+
class BooleanSequence
|
36
|
+
def self.from_soap(xml)
|
37
|
+
object = [*xml[:item]].map { |i| i == "true" }
|
38
|
+
return object.length == 1 ? object.first : object
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
34
43
|
# @private DoubleSequence
|
35
44
|
class DoubleSequence < BasicSequence
|
36
45
|
set_conversion_method :to_f
|
data/lib/icontrol/base/struct.rb
CHANGED
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'rexml/parsers/streamparser'
|
2
|
+
require 'rexml/parsers/baseparser'
|
3
|
+
require 'rexml/light/node'
|
4
|
+
require 'rexml/text'
|
5
|
+
require 'date'
|
6
|
+
require 'time'
|
7
|
+
require 'yaml'
|
8
|
+
require 'bigdecimal'
|
9
|
+
|
10
|
+
# This is a slighly modified version of the XMLUtilityNode from
|
11
|
+
# http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
|
12
|
+
# It's mainly just adding vowels, as I ht cd wth n vwls :)
|
13
|
+
# This represents the hard part of the work, all I did was change the
|
14
|
+
# underlying parser.
|
15
|
+
class REXMLUtilityNode #:nodoc:
|
16
|
+
attr_accessor :name, :attributes, :children, :type, :empty_node
|
17
|
+
|
18
|
+
def self.typecasts
|
19
|
+
@@typecasts
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.typecasts=(obj)
|
23
|
+
@@typecasts = obj
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.available_typecasts
|
27
|
+
@@available_typecasts
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.available_typecasts=(obj)
|
31
|
+
@@available_typecasts = obj
|
32
|
+
end
|
33
|
+
|
34
|
+
self.typecasts = {}
|
35
|
+
self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
|
36
|
+
self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
|
37
|
+
self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
|
38
|
+
self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
|
39
|
+
self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
|
40
|
+
self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)}
|
41
|
+
self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
|
42
|
+
self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
|
43
|
+
self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym}
|
44
|
+
self.typecasts["string"] = lambda{|v| v.to_s}
|
45
|
+
self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
|
46
|
+
self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
|
47
|
+
|
48
|
+
self.available_typecasts = self.typecasts.keys
|
49
|
+
|
50
|
+
def initialize(name, normalized_attributes = {})
|
51
|
+
|
52
|
+
# unnormalize attribute values
|
53
|
+
attributes = Hash[* normalized_attributes.map { |key, value|
|
54
|
+
[ key, unnormalize_xml_entities(value) ]
|
55
|
+
}.flatten]
|
56
|
+
|
57
|
+
@name = name.tr("-", "_")
|
58
|
+
# leave the type alone if we don't know what it is
|
59
|
+
@type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
|
60
|
+
|
61
|
+
@nil_element = attributes.delete("nil") == "true"
|
62
|
+
@attributes = undasherize_keys(attributes)
|
63
|
+
@children = []
|
64
|
+
@text = false
|
65
|
+
@empty_node = false
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_node(node)
|
69
|
+
@text = true if node.is_a? String
|
70
|
+
@children << node
|
71
|
+
end
|
72
|
+
|
73
|
+
def emptyze!(obj,value)
|
74
|
+
# Monkeypatch to allow empty_node? in the hash values
|
75
|
+
obj.class.instance_eval do
|
76
|
+
define_method(:empty_node?) { @_empty_node }
|
77
|
+
end
|
78
|
+
obj.instance_eval { @_empty_node = value }
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_hash
|
82
|
+
if @type == "file"
|
83
|
+
f = StringIO.new((@children.first || '').unpack('m').first)
|
84
|
+
class << f
|
85
|
+
attr_accessor :original_filename, :content_type
|
86
|
+
end
|
87
|
+
f.original_filename = attributes['name'] || 'untitled'
|
88
|
+
f.content_type = attributes['content_type'] || 'application/octet-stream'
|
89
|
+
return {name => f}
|
90
|
+
end
|
91
|
+
|
92
|
+
if @text
|
93
|
+
t = typecast_value( unnormalize_xml_entities( inner_html ) )
|
94
|
+
t.class.send(:attr_accessor, :attributes)
|
95
|
+
t.attributes = attributes
|
96
|
+
emptyze!(t,false)
|
97
|
+
return { name => t }
|
98
|
+
else
|
99
|
+
#change repeating groups into an array
|
100
|
+
groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
|
101
|
+
|
102
|
+
out = nil
|
103
|
+
if @type == "array"
|
104
|
+
out = []
|
105
|
+
groups.each do |k, v|
|
106
|
+
if v.size == 1
|
107
|
+
out << v.first.to_hash.entries.first.last
|
108
|
+
else
|
109
|
+
out << v.map{|e| e.to_hash[k]}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
out = out.flatten
|
113
|
+
|
114
|
+
else # If Hash
|
115
|
+
out = {}
|
116
|
+
groups.each do |k,v|
|
117
|
+
if v.size == 1
|
118
|
+
out.merge!(v.first)
|
119
|
+
else
|
120
|
+
out.merge!( k => v.map{|e| e.to_hash[k]})
|
121
|
+
end
|
122
|
+
end
|
123
|
+
out.merge! attributes unless attributes.empty?
|
124
|
+
out = out.empty? ? nil : out
|
125
|
+
end
|
126
|
+
|
127
|
+
if @type && out.nil?
|
128
|
+
out = typecast_value(out)
|
129
|
+
end
|
130
|
+
emptyze!(out,@empty_node)
|
131
|
+
|
132
|
+
{ name => out }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Typecasts a value based upon its type. For instance, if
|
137
|
+
# +node+ has #type == "integer",
|
138
|
+
# {{[node.typecast_value("12") #=> 12]}}
|
139
|
+
#
|
140
|
+
# @param value<String> The value that is being typecast.
|
141
|
+
#
|
142
|
+
# @details [:type options]
|
143
|
+
# "integer"::
|
144
|
+
# converts +value+ to an integer with #to_i
|
145
|
+
# "boolean"::
|
146
|
+
# checks whether +value+, after removing spaces, is the literal
|
147
|
+
# "true"
|
148
|
+
# "datetime"::
|
149
|
+
# Parses +value+ using Time.parse, and returns a UTC Time
|
150
|
+
# "date"::
|
151
|
+
# Parses +value+ using Date.parse
|
152
|
+
#
|
153
|
+
# @return <Integer, TrueClass, FalseClass, Time, Date, Object>
|
154
|
+
# The result of typecasting +value+.
|
155
|
+
#
|
156
|
+
# @note
|
157
|
+
# If +self+ does not have a "type" key, or if it's not one of the
|
158
|
+
# options specified above, the raw +value+ will be returned.
|
159
|
+
def typecast_value(value)
|
160
|
+
return value unless @type
|
161
|
+
proc = self.class.typecasts[@type]
|
162
|
+
proc.nil? ? value : proc.call(value)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Take keys of the form foo-bar and convert them to foo_bar
|
166
|
+
def undasherize_keys(params)
|
167
|
+
params.keys.each do |key, value|
|
168
|
+
params[key.tr("-", "_")] = params.delete(key)
|
169
|
+
end
|
170
|
+
params
|
171
|
+
end
|
172
|
+
|
173
|
+
# Get the inner_html of the REXML node.
|
174
|
+
def inner_html
|
175
|
+
@children.join
|
176
|
+
end
|
177
|
+
|
178
|
+
# Converts the node into a readable HTML node.
|
179
|
+
#
|
180
|
+
# @return <String> The HTML node in text form.
|
181
|
+
def to_html
|
182
|
+
attributes.merge!(:type => @type ) if @type
|
183
|
+
"<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
|
184
|
+
end
|
185
|
+
|
186
|
+
# @alias #to_html #to_s
|
187
|
+
def to_s
|
188
|
+
to_html
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def unnormalize_xml_entities value
|
194
|
+
REXML::Text.unnormalize(value)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
module Crack
|
199
|
+
class XML
|
200
|
+
def self.parse(xml)
|
201
|
+
stack = []
|
202
|
+
parser = REXML::Parsers::BaseParser.new(xml)
|
203
|
+
inner_elements = [0]
|
204
|
+
|
205
|
+
while true
|
206
|
+
event = parser.pull
|
207
|
+
case event[0]
|
208
|
+
when :end_document
|
209
|
+
break
|
210
|
+
when :end_doctype, :start_doctype
|
211
|
+
# do nothing
|
212
|
+
when :start_element
|
213
|
+
stack.push REXMLUtilityNode.new(event[1], event[2])
|
214
|
+
inner_elements[-1] += 1
|
215
|
+
inner_elements << 0
|
216
|
+
when :end_element
|
217
|
+
if stack.size > 1
|
218
|
+
temp = stack.pop
|
219
|
+
temp.empty_node = true if inner_elements.pop == 0
|
220
|
+
stack.last.add_node(temp)
|
221
|
+
else
|
222
|
+
stack.last.empty_node = true if inner_elements.pop == 0
|
223
|
+
end
|
224
|
+
when :text, :cdata
|
225
|
+
stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty?
|
226
|
+
end
|
227
|
+
end
|
228
|
+
stack.length > 0 ? stack.pop.to_hash : {}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|