icontrol 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|