diff --git a/CHANGELOG.textile b/CHANGELOG.textile index 44a2ef4..b33cc14 100644 --- a/CHANGELOG.textile +++ b/CHANGELOG.textile @@ -1,3 +1,12 @@ +*3.0* +* Substantially rewrote the ObjectFilter class. ObjectFilters used to be hashes which made it easy to manipulate their content incorrectly. The new implementation has a strict interface that makes it harder to manipulate filters incorrectly. +* Added a model for Virtual Server Image Templates (SoftLayer::ImageTemplate) - VirtualServerOrder now requires an instance of this class rather than allowing you to provide the global_id of an image +* Added a model for data centers (SoftLayer::Datacenter). Bare Metal, Bare Metal Package, and Virtual server orders now use an instance of Datacenter to identify where their servers will be provisioned. The routines in those classes which used to provide lists of valid data center names now return data center objects. +* Virtual Server Upgrades are now handled by the VirtualServerUpgradeOrder class and not the VirtualServer class. This change was made for several reasons. Firt and foremost, it allows multiple aspects of a virtual server to be upgraded at once without having to wait on separate transactions to complete between upgrades. Secondly it opens the door for additional upgrades (for example, to disk configuration) to be added in the future. +* Added a method to reboot servers. +* The routine to retreive the open tickets on an account has been moved from the Ticket class. The set of open tickets is now a dynamic property of an account object. +* The Model Layer now includes models for Server (aka. Shared) and VLAN (aka. Dedicated) firewalls in the ServerFirewall, and VLANFireall classes respectively. There are corresponding classes for ordering firewalls (ServerFirewallOrder and VLANFirewallOrder). To facilitate the process of locating the 'id' for a firewall, the Account class includes the find_VLAN_with_number routine which lets you look up the segments of a firewall from the VLAN nubmer. + *2.2.2* * Fixed a bug in BareMetalServerOrder_Package.rb where the order template did not use an array for the "hardware" key. This lead to an order template that would be accepted by verifyOrder, but rejected by placeOrder. An internal issue to review verifyOrder has also been generated. (reported by Rohit Singh) @@ -7,7 +16,7 @@ *2.1.1* * Virtual server upgrades no longer raise exceptions -* Formalized the RDoc documentation process. Added overview and welcome documentation and changed the README so it directs folks to the new documentation. +* Formalized the RDoc documentation process. Added overview and welcome documentation and changed the README so it directs folks to the new documentation. *2.1.0* * Began implementing a model framework that allows Ruby developers to work with elements in the SoftLayer API in a more object-oriented fashion. The first release of this framework includes the Ticket, VirtualServer, and BareMetalServer classes. diff --git a/README.md b/README.md index 6359ff3..5b61fa1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ This software is written by the SoftLayer Development Team [sldn@softlayer.com]( Please join us in the [SoftLayer Developer Network forums](http://forums.softlayer.com/forum/softlayer-developer-network) +# Contributer License Agreement + +Contributions to the softlayer-ruby project require the submission of a contributer license agreement. Please generate the documentation and carefully refer to the Contribution Guide to participate. + # Copyright and License The `softlayer_api` Ruby Gem and it's source files are Copyright © 2010-2014 [SoftLayer Technologies, Inc](http://www.softlayer.com/). The software is provided under an MIT license. Details of that license can be found in the embedded LICENSE.md file. diff --git a/doc_src/Contribution Guide.md b/doc_src/Contribution Guide.md index 699bcf4..49c29ee 100644 --- a/doc_src/Contribution Guide.md +++ b/doc_src/Contribution Guide.md @@ -15,7 +15,7 @@ Any requests for enhancements, new features, or bug reports should be entered in # Development Environment -As a Ruby project, your first step will be to install the [Ruby Programming Language](https://www.ruby-lang.org/en/). Many Unix-derived environments, including Mac OS X, have a version or Ruby installed by default, however, the default version may be out-of-date. Please visit the main Ruby language [site](https://www.ruby-lang.org/en/) for instructions on installing an up-to-date build of Ruby for your computing environment. +As a Ruby project, your first step will be to install the [Ruby Programming Language](https://www.ruby-lang.org/en/). Many Unix-derived environments, including Mac OS X, have a version or Ruby installed by default, however, the default version may be out-of-date. Please visit the main Ruby language [site](https://www.ruby-lang.org/en/) for instructions on installing an up-to-date build of Ruby for your computing environment. The Gem supports multiple versions of Ruby, and we recommend using Ruby 2.0 or later. The [Ruby Version Manager (rvm)](https://rvm.io) is an invaluable tool to help keep track of multiple versions of Ruby. The Gem no longer supports Ruby 1.8.7. Support for Ruby 1.9 will continue for a time, but the Core Ruby team is already withdrawing their support for that version. @@ -100,7 +100,7 @@ The basic directory structure for the source tree is as follows softlayer # Folder containing most of the gem's actual source code log # RVM will create a log folder when running commands across multiple ruby versions. pkg # Created when the gem is built, contains built versions of the gem - spec # Source directory for the RSpec testing specifications + spec # Source directory for the RSpec testing specifications fixtures # Files used by the unit tests to mock responses from the SoftLayer network API Most of the source files that implement the gem are found in `lib/softlayer`. If you wish to add new functionality, or edit existing functionality, you will probably edit the class files in this directory. Unit tests using Rspec are found in the spec folder and should generally follow the naming convention of _spec.rb diff --git a/doc_src/Foundation.md b/doc_src/Foundation.md index d8f62a3..99871f6 100644 --- a/doc_src/Foundation.md +++ b/doc_src/Foundation.md @@ -21,7 +21,7 @@ SoftLayer provides two different endpoint URLs to scripts. One is associated wit # The base URL of the SoftLayer API available to the public internet. API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/' - + # The base URL of the SoftLayer API available through SoftLayer's private network API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3/' @@ -65,7 +65,7 @@ The open_tickets variable should receive an array of hashes representing the ope softlayer_client = SoftLayer::Client.new( :username => "joecustomer", api_key => "feeddeadbeefbadf00d...) open_tickets = softlayer_client["Account"].getOpenTickets - + open_tickets.each { |ticket_hash| puts ticket_hash["title"] } This short example shows the essence of working with the Foundation API, you create a client, obtain a service, and make calls to the network SoftLayer API through that service. @@ -82,7 +82,7 @@ Calls to the network SoftLayer API that result in errors being returned by the s ## Troubleshooting -Communication with the SoftLayer servers is handled through the XML-RPC client that is built into the Ruby Core library. As a consequence the network communication is also handled by Core library classes. +Communication with the SoftLayer servers is handled through the XML-RPC client that is built into the Ruby Core library. As a consequence the network communication is also handled by Core library classes. One aspect of network communication that the `softlayer_api` relies on the Ruby Core library to provide is SSL certificate authentication. Problems with this authentication often arise if your Ruby environment is not properly configured with SSL root certificates. If you find you are having trouble communicating with the network SoftLayer API, and the error messages point to SSL certificate authentication, please consider a web search using your specific error message as a search string. This will often reveal answers that can help you resolve networking issues your Ruby environment. diff --git a/doc_src/Model Layer.md b/doc_src/Model Layer.md index 60cb8e5..ab8f5d7 100644 --- a/doc_src/Model Layer.md +++ b/doc_src/Model Layer.md @@ -10,7 +10,7 @@ The details of the individual classes that for the object class hierarchy of the The ModelBase is the abstract base class of object class hierarchy that forms the Model Layer of the `softlayer_api` Gem. An instance of ModelBase represents a single entity within the SoftLayer API. -In the Foundation layer, SoftLayer entities are represented as a Ruby hash whose keys and values are the are property names and property values of the entity. In the Model Layer, SoftLayer entities are represented by instances of the concrete subclasses of the Model Base class. +In the Foundation layer, SoftLayer entities are represented as a Ruby hash whose keys and values are the are property names and property values of the entity. In the Model Layer, SoftLayer entities are represented by instances of the concrete subclasses of the Model Base class. In implementation terms, an instance of the ModelBase class (or more accurately and instance of a concrete subclass of the ModelBase class) encapsulates the hashes of the Foundation layer defines the attributes and operations that form a convenient model for working with the underlying entity. @@ -24,7 +24,7 @@ The initializer for classes in the ModelBase hierarchy are declared: … end -The first argument is the client that the object may use to make requests to the network API. The second is the `network_hash`, the hash representation of the entity as returned by the network API. +The first argument is the client that the object may use to make requests to the network API. The second is the `network_hash`, the hash representation of the entity as returned by the network API. The hash used to initialize an instance of ModelBase *must* contain a key, `id`, whose value is the `id` of the SoftLayer entity that the object model instance will represent. Correspondingly, the ModelBase class defines the `id` as having the same value as the `id` property in the network hash. @@ -43,7 +43,7 @@ The ModelBase class defines the subscript operator (`[]`) to accept a property n ticket = SoftLayer::Ticket.ticket_with_id(123456) service_provider = ticket['serviceProvider'] -In this case we ask the ticket for the value of the `serviceProvider` property. Note that the argument to the subscript operator is a string containing the property name. +In this case we ask the ticket for the value of the `serviceProvider` property. Note that the argument to the subscript operator is a string containing the property name. This technique can only return values stored in the `softlayer_hash` encapsulated in the ModelBase class. Many classes in the Model Layer limit the information retrieved from the network (using object masks) to a subset of the full set of properties available through the network API. Scripts can check whether or not a given property is included in the underlying hash by calling the `has_sl_property?` method of ModelBase. diff --git a/doc_src/Welcome.md b/doc_src/Welcome.md index 1a17b97..26c1826 100644 --- a/doc_src/Welcome.md +++ b/doc_src/Welcome.md @@ -18,6 +18,6 @@ The Foundation layer, makes use of the [XMLRPC client](http://www.ruby-doc.org/s The Model layer is built atop the foundation as object class hierarchy. The class hierarchy models the structures found in the SoftLayer environment using the object-oriented features of Ruby. It does this to abstract out some of the implementation detail that a developer would commonly have to work with to communicate with SoftLayer through the foundation layer. -The Model layer is by no means complete; quite to the contrary it is in its infancy and we believe that much of the development effort in the Gem will focus on incorporating new models into this layer. Because it is incomplete, however, we have put some effort into bridges from the functionality of the model, down to the lower level foundation, without trouble. Also, as a result of this, developers interested in using the Model layer should also should familiarize themselves with the Foundation. +The Model layer is by no means complete; quite to the contrary it is in its infancy and we believe that much of the development effort in the Gem will focus on incorporating new models into this layer. Because it is incomplete, however, we have put some effort into bridges from the functionality of the model, down to the lower level foundation, without trouble. Also, as a result of this, developers interested in using the Model layer should also should familiarize themselves with the Foundation. All developers should continue their exploration of the `softlayer_api` gem by examining the Foundation documentation. Clients that wish to make use of the abstractions provided in the object hierarchy may continue their exploration by looking at the Model Layer documentation. Developers who wish to expand the models found in the `softlayer_api` Gem should read the [Contribution Guide](ContributionGuide_md.html) \ No newline at end of file diff --git a/examples/account_servers.rb b/examples/account_servers.rb index 93f8570..9d0a52a 100644 --- a/examples/account_servers.rb +++ b/examples/account_servers.rb @@ -23,8 +23,8 @@ require 'rubygems' require 'softlayer_api' require 'pp' - - # We can set the default client to be our client and that way + + # We can set the default client to be our client and that way # we can avoid supplying it later SoftLayer::Client.default_client = SoftLayer::Client.new( # :username => "joecustomer" # enter your username here diff --git a/examples/create_ticket.rb b/examples/create_ticket.rb index b60775d..231f7ed 100755 --- a/examples/create_ticket.rb +++ b/examples/create_ticket.rb @@ -35,7 +35,7 @@ account = SoftLayer::Account.account_for_client(softlayer_client) account_user = account.service.getCurrentUser my_user_id = account_user["id"] - + # We also need a subject for the ticket. Subjects are specified by id # This code prints out a table of all the ticket subjects with their # ids: @@ -43,7 +43,7 @@ ticket_subjects.each do |subject| puts "#{subject['id']}\t#{subject['name']}" end - + # For this example we'll use 'Public Network Question' as the subject. That's id 1022 public_network_question_id = 1022 @@ -58,7 +58,7 @@ ) puts "Created a new ticket : #{new_ticket.id} - #{new_ticket.title}" - + # we can also add an update to the ticket: new_ticket.update("This is a ticket update sent from the Ruby library") diff --git a/examples/open_tickets.rb b/examples/open_tickets.rb index 7aa82e2..4f469b1 100644 --- a/examples/open_tickets.rb +++ b/examples/open_tickets.rb @@ -30,14 +30,14 @@ # $SL_API_KEY = "feeddeadbeefbadf00d..." # enter your api key here # The client constructed here must get it's credentials from somewhere -# In this script you might uncomment the globals above and assign your +# In this script you might uncomment the globals above and assign your # credentials there SoftLayer::Client.default_client = SoftLayer::Client.new() # The openTickets routine will pick up the default client established above. open_tickets = SoftLayer::Ticket.open_tickets() -open_tickets.sort!{ |lhs, rhs| -(lhs.lastEditDate <=> rhs.lastEditDate) } +open_tickets.sort!{ |lhs, rhs| -(lhs.lastEditDate <=> rhs.lastEditDate) } open_tickets.each do |ticket| printf "#{ticket.id} - #{ticket.title}" diff --git a/examples/order_bare_metal_package.rb b/examples/order_bare_metal_package.rb index d13cc97..e49ce17 100644 --- a/examples/order_bare_metal_package.rb +++ b/examples/order_bare_metal_package.rb @@ -129,11 +129,11 @@ def tl_dr_version # We have a configuration for the server, we also need a location for the new server. # The package can give us a list of locations. Let's print out that list puts "\nData Centers for '#{quad_intel_package.name}':" - quad_intel_package.datacenter_options.each { |location| puts "\t#{location}"} + quad_intel_package.datacenter_options.each { |datacenter| puts "\t#{datacenter.name}"} # With all the config options in place we can now construct the product order. server_order = SoftLayer::BareMetalServerOrder_Package.new(quad_intel_package, client) - server_order.datacenter = 'sng01' + server_order.datacenter = SoftLayer::Datacenter.datacenter_named 'sng01', client server_order.hostname = 'sample' server_order.domain = 'softlayerapi.org' server_order.configuration_options = config_options diff --git a/examples/order_server_firewall.rb b/examples/order_server_firewall.rb new file mode 100644 index 0000000..9d41bf5 --- /dev/null +++ b/examples/order_server_firewall.rb @@ -0,0 +1,63 @@ +# +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +require 'rubygems' +require 'softlayer_api' +require 'pp' + +# This is the id of the server you want to protect with a firewall. +# The server can be Bare Metal or Virtual. It should have a public +# network interface, and it should not already have a firewall on it. +server_id = 257696 # 12345 + +# In this example, we assume this is a Bare Metal Server +is_virtual_server = false + +# Work with the SoftLayer API begins with a client. By setting +# the "default" client we avoid having to specify the client repeatedly +# in calls that follow. +SoftLayer::Client.default_client = SoftLayer::Client.new( + # :username => "joecustomer" # enter your username here + # :api_key => "feeddeadbeefbadf00d..." # enter your api key here +) + +# in this case we go straight to the appropriate class to find the server +# an alternative might be to create the account for this client and +# search the list of servers for the one with the appropriate ID. +if is_virtual_server + server = SoftLayer::VirtualServer.server_with_id(server_id) +else + server = SoftLayer::BareMetalServer.server_with_id(server_id) +end + +# Create an instance of SoftLayer::ServerFirewallOrder +order = SoftLayer::ServerFirewallOrder.new(server) + +begin + # this example calls order.verify which will build the order, submit it + # to the network API, and will throw an exception if the order is + # invalid. + order.verify() + puts "Firewall order is good for #{server.fullyQualifiedDomainName}" +rescue => exception + puts "Firewall order failed for #{server.fullyQualifiedDomainName} because #{exception}" +end \ No newline at end of file diff --git a/examples/ticket_info.rb b/examples/ticket_info.rb index 197d355..31d31f9 100644 --- a/examples/ticket_info.rb +++ b/examples/ticket_info.rb @@ -34,13 +34,13 @@ # at information. In this case we are talking directly to the ticket # service ticket_service = softlayer_client.service_named("Ticket"); - + # Retrive a particular ticket by ID (you will have to substitute an existing ticket's ID here) ticket_ref = ticket_service.object_with_id(12345) # Retrive very specific information about the ticket ticket = ticket_ref.object_mask("mask[updates[entry,createDate],assignedUserId,attachedHardware.datacenter]").getObject - + pp ticket rescue Exception => exception puts "Unable to retrieve the ticket #{exception}" diff --git a/lib/softlayer/APIParameterFilter.rb b/lib/softlayer/APIParameterFilter.rb index 631386f..3f72282 100644 --- a/lib/softlayer/APIParameterFilter.rb +++ b/lib/softlayer/APIParameterFilter.rb @@ -1,24 +1,10 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ + + module SoftLayer # An +APIParameterFilter+ is an intermediary object that understands how @@ -57,6 +43,13 @@ def initialize(target, starting_parameters = nil) @parameters = starting_parameters || {} end + ## + # API Parameter filters will call through to a particular service + # but that service is defined by their target + def service_name + return @target.service_name + end + ## # Adds an API filter that narrows the scope of a call to an object with # a particular ID. For example, if you want to get the ticket @@ -119,7 +112,7 @@ def result_limit(offset, limit) # to specify criteria which are used to filter the results returned # by the server. def object_filter(filter) - raise ArgumentError, "Object mask expects mask properties" if filter.nil? + raise ArgumentError, "object_filter expects an instance of SoftLayer::ObjectFilter" if filter.nil? || !filter.kind_of?(SoftLayer::ObjectFilter) # we create a new object in case the user wants to store off the # filter chain and reuse it later @@ -187,7 +180,7 @@ def server_result_offset ## # A utility method that returns the object filter (if any) stored with this filter. def server_object_filter - self.parameters[:object_filter] + self.parameters[:object_filter].to_h if self.parameters.has_key?(:object_filter) end ## diff --git a/lib/softlayer/Account.rb b/lib/softlayer/Account.rb index 828cca2..3c5a8fe 100644 --- a/lib/softlayer/Account.rb +++ b/lib/softlayer/Account.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer class Account < SoftLayer::ModelBase @@ -113,8 +97,51 @@ class Account < SoftLayer::ModelBase end end + sl_dynamic_attr :image_templates do |image_templates| + image_templates.should_update? do + @last_image_template_update ||= Time.at(0) + (Time.now - @last_image_template_update) > 5 * 60 # update every 5 minutes + end + + image_templates.to_update do + @last_image_template_update ||= Time.now + ImageTemplate.find_private_templates(:client => self.softlayer_client) + end + end + + sl_dynamic_attr :open_tickets do |open_tickets| + open_tickets.should_update? do + @last_open_tickets_update ||= Time.at(0) + (Time.now - @last_open_tickets_update) > 5 * 60 # update every 5 minutes + end + + open_tickets.to_update do + @last_open_tickets_update ||= Time.now + open_tickets_data = self.service.object_mask(SoftLayer::Ticket.default_object_mask).getOpenTickets + open_tickets_data.collect { |ticket_data| SoftLayer::Ticket.new(self.softlayer_client, ticket_data) } + end + end + def service - softlayer_client["Account"].object_with_id(self.id) + softlayer_client[:Account].object_with_id(self.id) + end + + ## + # Searches the account's list of VLANs for the ones with the given + # vlan number. This may return multiple results because a VLAN can + # span different routers and you will get a separate segment for + # each router. + # + # The IDs of the different segments can be helpful for ordering + # firewalls. + # + def find_VLAN_with_number(vlan_number) + filter = SoftLayer::ObjectFilter.new() { |filter| + filter.accept('networkVlans.vlanNumber').when_it is vlan_number + } + + vlan_data = self.service.object_mask("mask[id,vlanNumber,primaryRouter,networkSpace]").object_filter(filter).getNetworkVlans + return vlan_data end ## @@ -125,7 +152,7 @@ def self.account_for_client(client = nil) softlayer_client = client || Client.default_client raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - account_service = softlayer_client['Account'] + account_service = softlayer_client[:Account] network_hash = account_service.getObject() new(softlayer_client, network_hash) end diff --git a/lib/softlayer/BareMetalServer.rb b/lib/softlayer/BareMetalServer.rb index ebe36e0..f94af7e 100644 --- a/lib/softlayer/BareMetalServer.rb +++ b/lib/softlayer/BareMetalServer.rb @@ -1,24 +1,10 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ + + module SoftLayer # @@ -27,6 +13,7 @@ module SoftLayer # +SoftLayer_Hardware_Server+ services in the SoftLayer API # # http://sldn.softlayer.com/reference/datatypes/SoftLayer_Hardware + # # http://sldn.softlayer.com/reference/datatypes/SoftLayer_Hardware_Server # class BareMetalServer < Server @@ -40,7 +27,7 @@ class BareMetalServer < Server # def bare_metal_instance? if has_sl_property?(:bareMetalInstanceFlag) - self["bareMetalInstanceFlag"] != 0 + self['bareMetalInstanceFlag'] != 0 else false end @@ -51,7 +38,7 @@ def bare_metal_instance? # removed from the account). # # The +cancellation_reason+ parameter should be a key from the hash returned - # by +BareMetalServer::cancellation_reasons+. + # by BareMetalServer::cancellation_reasons. # # You may add your own, more specific reasons for cancelling a server in the # +comments+ parameter. @@ -60,19 +47,20 @@ def cancel!(reason = :unneeded, comment = '') if !bare_metal_instance? then cancellation_reasons = self.class.cancellation_reasons() cancel_reason = cancellation_reasons[reason] || cancellation_reasons[:unneeded] - softlayer_client["Ticket"].createCancelServerTicket(self.id, cancel_reason, comment, true, 'HARDWARE') + softlayer_client[:Ticket].createCancelServerTicket(self.id, cancel_reason, comment, true, 'HARDWARE') else # Note that reason and comment are ignored in this case, unfortunately - softlayer_client['Billing_Item'].object_with_id(self.billingItem['id'].to_i).cancelService() + softlayer_client[:Billing_Item].object_with_id(self.billingItem['id'].to_i).cancelService() end end ## - # Returns the SoftLayer Service used to work with this Server + # Returns the typical Service used to work with this Server # For Bare Metal Servers that is +SoftLayer_Hardware+ though in some special cases - # you may have to use +SoftLayer_Hardware_Server+ as a type or service. + # you may have to use +SoftLayer_Hardware_Server+ as a type or service. That + # service object is available thorugh the hardware_server_service method def service - return softlayer_client["Hardware"].object_with_id(self.id) + return softlayer_client[:Hardware_Server].object_with_id(self.id) end ## @@ -119,6 +107,36 @@ def self.cancellation_reasons } end + ## + # Returns the max port speed of the public network interfaces of the server taking into account + # bound interface pairs (redundant network cards). + def firewall_port_speed + network_components = self.service.object_mask("mask[id,maxSpeed,networkComponentGroup.networkComponents]").getFrontendNetworkComponents() + + # Split the interfaces into grouped and ungrouped interfaces. The max speed of a group will be the sum + # of the individual speeds in that group. The max speed of ungrouped interfaces is simply the max speed + # of that interface. + grouped_interfaces, ungrouped_interfaces = network_components.partition{ |interface| interface.has_key?("networkComponentGroup") } + + if !grouped_interfaces.empty? + group_speeds = grouped_interfaces.collect do |interface| + interface['networkComponentGroup']['networkComponents'].inject(0) {|total_speed, component| total_speed += component['maxSpeed']} + end + + max_group_speed = group_speeds.max + else + max_group_speed = 0 + end + + if !ungrouped_interfaces.empty? + max_ungrouped_speed = ungrouped_interfaces.collect { |interface| interface['maxSpeed']}.max + else + max_ungrouped_speed = 0 + end + + return [max_group_speed, max_ungrouped_speed].max + end + ## # Retrive the bare metal server with the given server ID from the # SoftLayer API @@ -133,7 +151,7 @@ def self.server_with_id(server_id, options = {}) softlayer_client = options[:client] || Client.default_client raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - hardware_service = softlayer_client["Hardware"] + hardware_service = softlayer_client[:Hardware_Server] hardware_service = hardware_service.object_mask(default_object_mask.to_sl_object_mask) if options.has_key?(:object_mask) @@ -178,8 +196,9 @@ def self.find_servers(options_hash = {}) if(options_hash.has_key? :object_filter) object_filter = options_hash[:object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless object_filter.kind_of?(SoftLayer::ObjectFilter) else - object_filter = {} + object_filter = ObjectFilter.new() end option_to_filter_path = { @@ -197,21 +216,21 @@ def self.find_servers(options_hash = {}) # that particular option, add a clause to the object filter that filters for the matching # value option_to_filter_path.each do |option, filter_path| - object_filter.merge!(SoftLayer::ObjectFilter.build(filter_path, options_hash[option])) if options_hash.has_key?(option) + object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option])} if options_hash[option] end # Tags get a much more complex object filter operation so we handle them separately if options_hash.has_key?(:tags) - object_filter.merge!(SoftLayer::ObjectFilter.build("hardware.tagReferences.tag.name", { + object_filter.set_criteria_for_key_path("hardware.tagReferences.tag.name", { 'operation' => 'in', 'options' => [{ 'name' => 'data', - 'value' => options_hash[:tags] + 'value' => options_hash[:tags].collect{ |tag_value| tag_value.to_s } }] - } )); + } ); end - account_service = softlayer_client['Account'] + account_service = softlayer_client[:Account] account_service = account_service.object_filter(object_filter) unless object_filter.empty? account_service = account_service.object_mask(default_object_mask.to_sl_object_mask) diff --git a/lib/softlayer/BareMetalServerOrder.rb b/lib/softlayer/BareMetalServerOrder.rb index 3e924fe..9be737c 100644 --- a/lib/softlayer/BareMetalServerOrder.rb +++ b/lib/softlayer/BareMetalServerOrder.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer # @@ -46,8 +30,7 @@ class BareMetalServerOrder # a Bare Metal Instance #++ - # String, short name of the data center that will house the new Bare Metal Instance (e.g. "dal05" or "sea01") - # Corresponds to +datacenter.name+ in the documentation for +createObject+. + # An instance of SoftLayer::Datacenter. The server will be provisioned in this data center attr_accessor :datacenter # String, The hostname to assign to the new server @@ -128,7 +111,7 @@ def verify() order_template = hardware_instance_template order_template = yield order_template if block_given? - @softlayer_client["Hardware"].generateOrderTemplate(order_template) + @softlayer_client[:Hardware].generateOrderTemplate(order_template) end ## @@ -141,8 +124,8 @@ def place_order!() order_template = hardware_instance_template order_template = yield order_template if block_given? - server_hash = @softlayer_client["Hardware"].createObject(order_template) - SoftLayer::BareMetalServer.server_with_id(server_hash["id"], :client => @softlayer_client) if server_hash + server_hash = @softlayer_client[:Hardware].createObject(order_template) + SoftLayer::BareMetalServer.server_with_id(server_hash['id'], :client => @softlayer_client) if server_hash end protected @@ -164,15 +147,15 @@ def hardware_instance_template "hourlyBillingFlag" => !!@hourly } - template["privateNetworkOnlyFlag"] = true if @private_network_only + template['privateNetworkOnlyFlag'] = true if @private_network_only - template["datacenter"] = {"name" => @datacenter} if @datacenter + template['datacenter'] = {"name" => @datacenter.name} if @datacenter template['userData'] = [{'value' => @user_metadata}] if @user_metadata template['networkComponents'] = [{'maxSpeed'=> @max_port_speed}] if @max_port_speed template['postInstallScriptUri'] = @provision_script_URI.to_s if @provision_script_URI template['sshKeys'] = @ssh_key_ids.collect { |ssh_key| {'id'=> ssh_key.to_i } } if @ssh_key_ids template['primaryNetworkComponent'] = { "networkVlan" => { "id" => @public_vlan_id.to_i } } if @public_vlan_id - template["primaryBackendNetworkComponent"] = { "networkVlan" => {"id" => @private_vlan_id.to_i } } if @private_vlan_id + template['primaryBackendNetworkComponent'] = { "networkVlan" => {"id" => @private_vlan_id.to_i } } if @private_vlan_id if @disks && !@disks.empty? template['hardDrives'] = @disks.collect do |disk| @@ -191,36 +174,36 @@ def self.create_object_options(client = nil) raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client @@create_object_options ||= nil - @@create_object_options = softlayer_client["Hardware"].getCreateObjectOptions() if !@@create_object_options + @@create_object_options = softlayer_client[:Hardware].getCreateObjectOptions() if !@@create_object_options @@create_object_options end ## # Return a list of values that are valid for the :datacenter attribute def self.datacenter_options(client = nil) - create_object_options(client)["datacenters"].collect { |datacenter_spec| datacenter_spec['template']['datacenter']["name"] }.uniq.sort! + create_object_options(client)['datacenters'].collect { |datacenter_spec| Datacenter.datacenter_named(datacenter_spec['template']['datacenter']['name'], client) }.uniq end def self.core_options(client = nil) - create_object_options(client)["processors"].collect { |processor_spec| processor_spec['template']['processorCoreAmount'] }.uniq.sort! + create_object_options(client)['processors'].collect { |processor_spec| processor_spec['template']['processorCoreAmount'] }.uniq.sort! end ## # Return a list of values that are valid the array given to the :disks def self.disk_options(client = nil) - create_object_options(client)["hardDrives"].collect { |disk_spec| disk_spec['template']['hardDrives'][0]['capacity'].to_i}.uniq.sort! + create_object_options(client)['hardDrives'].collect { |disk_spec| disk_spec['template']['hardDrives'][0]['capacity'].to_i}.uniq.sort! end ## # Returns a list of the valid :os_refrence_codes def self.os_reference_code_options(client = nil) - create_object_options(client)["operatingSystems"].collect { |os_spec| os_spec['template']['operatingSystemReferenceCode'] }.uniq.sort! + create_object_options(client)['operatingSystems'].collect { |os_spec| os_spec['template']['operatingSystemReferenceCode'] }.uniq.sort! end ## # Returns a list of the :max_port_speeds def self.max_port_speed_options(client = nil) - create_object_options(client)["networkComponents"].collect { |component_spec| component_spec['template']['networkComponents'][0]['maxSpeed'] } + create_object_options(client)['networkComponents'].collect { |component_spec| component_spec['template']['networkComponents'][0]['maxSpeed'] } end end # class BareMetalServerOrder diff --git a/lib/softlayer/BareMetalServerOrder_Package.rb b/lib/softlayer/BareMetalServerOrder_Package.rb index 900f91a..61e0659 100644 --- a/lib/softlayer/BareMetalServerOrder_Package.rb +++ b/lib/softlayer/BareMetalServerOrder_Package.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer # @@ -42,13 +26,14 @@ module SoftLayer class BareMetalServerOrder_Package < Server # The following properties are required in a server order. - # The product package identifying the base configuration for the server. - # a list of Bare Metal Server product packages is returned by + # The product package object (an instance of SoftLayer::ProductPackage) identifying the base + # configuration for the server. A list of Bare Metal Server product packages is returned by # SoftLayer::ProductPackage.bare_metal_server_packages attr_reader :package - # String, short name of the data center that will house the new virtual server (e.g. "dal05" or "sea01") - # A list of valid data centers can be found in ProductPackage#datacenter_options + # An instance of SoftLayer::Datacenter. The server will be provisioned in this data center. + # The set of datacenters available is determined by the package and may be obtained from + # the SoftLayer::ProductPackage object using the #datacenter_options method. attr_accessor :datacenter # The hostname of the server being created (i.e. 'sldn' is the hostname of sldn.softlayer.com). @@ -73,7 +58,7 @@ class BareMetalServerOrder_Package < Server # An array of the ids of SSH keys to install on the server upon provisioning # To obtain a list of existing SSH keys, call getSshKeys on the SoftLayer_Account service: - # client['Account'].getSshKeys() + # client[:Account].getSshKeys() attr_accessor :ssh_key_ids # The URI of a script to execute on the server after it has been provisioned. This may be @@ -103,7 +88,7 @@ def initialize(package, client = nil) def verify product_order = hardware_order product_order = yield product_order if block_given? - softlayer_client["Product_Order"].verifyOrder(product_order) + softlayer_client[:Product_Order].verifyOrder(product_order) end ## @@ -121,7 +106,7 @@ def verify def place_order! product_order = hardware_order product_order = yield product_order if block_given? - softlayer_client["Product_Order"].placeOrder(product_order) + softlayer_client[:Product_Order].placeOrder(product_order) end protected @@ -139,8 +124,7 @@ def hardware_order }] } - product_order['location'] = @package.location_id_for_datacenter_name(@datacenter.downcase) if @datacenter - + product_order['location'] = @datacenter.id if @datacenter product_order['sshKeys'] = [{ 'sshKeyIds' => @ssh_key_ids }] if @ssh_key_ids product_order['provisionScripts'] = [@provision_script_URI.to_s] if @provision_script_URI @@ -156,7 +140,5 @@ def hardware_order product_order end - end # BareMetalServerOrder_Package - end # SoftLayer \ No newline at end of file diff --git a/lib/softlayer/Client.rb b/lib/softlayer/Client.rb index 849eeb1..bc32293 100644 --- a/lib/softlayer/Client.rb +++ b/lib/softlayer/Client.rb @@ -1,31 +1,18 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer - # Initialize an instance of the Client class. You pass in the service name - # and optionally hash arguments specifying how the client should access the - # SoftLayer API. + # A client is responsible for storing authentication information for API calls and + # it serves as a centeral repository for the Service instances that call into the + # network API. + # + # When you create a client, you pass in hash arguments specifying how the client + # should access the SoftLayer API. # - # The following symbols can be used as hash arguments to pass options to the constructor: + # The following symbols are the keys for options you pass to the constructor: # - +:username+ - a non-empty string providing the username to use for requests to the client # - +:api_key+ - a non-empty string providing the api key to use for requests to the client # - +:endpoint_url+ - a non-empty string providing the endpoint URL to use for requests to the client @@ -45,7 +32,7 @@ class Client # A string passsed as the value for the User-Agent header when requests are sent to SoftLayer API. attr_accessor :user_agent - + # An integer value (in seconds). The number of seconds to wait for HTTP requests to the network API # until they timeout. This value can be nil in which case the timeout will be the default value for # the library handling network communication (often 30 seconds) @@ -56,6 +43,13 @@ class Client # will be used by many methods if you do not provide an explicit client. @@default_client = nil + ## + # :attr_accessor: + # The client class can maintain a single instance of Client as the "default client" + # Other parts of the library that accept a client as part of their calling sequence + # will look for the default client if one is not provided in the call + # + # This routine returns the client set as the default client. It can be nil def self.default_client return @@default_client end @@ -90,8 +84,10 @@ def initialize(options = {}) # and the endpoint url @endpoint_url = settings[:endpoint_url] || API_PUBLIC_ENDPOINT + # set the user agent to the one provided, or set it to a default one @user_agent = settings[:user_agent] || "softlayer_api gem/#{SoftLayer::VERSION} (Ruby #{RUBY_PLATFORM}/#{RUBY_VERSION})" - + + # and assign a time out if the settings offer one @network_timeout = settings[:timeout] if settings.has_key?(:timeout) raise "A SoftLayer Client requires a username" if !@username || @username.empty? diff --git a/lib/softlayer/Config.rb b/lib/softlayer/Config.rb index 00776cb..c892ab0 100644 --- a/lib/softlayer/Config.rb +++ b/lib/softlayer/Config.rb @@ -1,34 +1,18 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ require 'configparser' module SoftLayer - # The SoftLayer Config class is responsible for providing the key information + # The SoftLayer Config class is responsible for providing the key information # the library needs to communicate with the network SoftLayer API. Those three crucial # pieces of information are the Username, the API Key, and the endpoint_url. This information # is collected in a hash with the keys `:username`, `:api_key`, and `:endpoint_url` repsectively. - # + # # The routine used to retrieve this information from a Config object is Config.client_settings # # There are several locations that the Config class looks for this information: @@ -58,7 +42,7 @@ module SoftLayer # # = Environment Variables # - # The config class will search the environment variables SL_USERNAME and SL_API_KEY for + # The config class will search the environment variables SL_USERNAME and SL_API_KEY for # the username and API key respectively. The endpoint_url may not be set thorugh # environment variables. # @@ -88,8 +72,8 @@ def Config.globals_settings def Config.environment_settings result = {} - result[:username] = ENV["SL_USERNAME"] if ENV["SL_USERNAME"] - result[:api_key] = ENV["SL_API_KEY"] if ENV["SL_API_KEY"] + result[:username] = ENV['SL_USERNAME'] if ENV['SL_USERNAME'] + result[:api_key] = ENV['SL_API_KEY'] if ENV['SL_API_KEY'] result end @@ -105,7 +89,7 @@ def Config.file_settings(*additional_files) search_path.each do |file_path| if File.readable? file_path config = ConfigParser.new file_path - softlayer_section = config["softlayer"] + softlayer_section = config['softlayer'] if softlayer_section result[:username] = softlayer_section['username'] if softlayer_section['username'] diff --git a/lib/softlayer/Datacenter.rb b/lib/softlayer/Datacenter.rb new file mode 100644 index 0000000..152bf21 --- /dev/null +++ b/lib/softlayer/Datacenter.rb @@ -0,0 +1,53 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # A Data Center in the SoftLayer network + # + # This class corresponds to the SoftLayer_Location++ data type: + # + # http://sldn.softlayer.com/reference/datatypes/SoftLayer_Location + # + # Although in this context it is used to represent a data center and + # not the more general class of locations that that data type can + # represent. + + class Datacenter < SoftLayer::ModelBase + sl_attr :name + sl_attr :long_name, "longName" + + ## + # Return the datacenter with the given name ('sng01' or 'dal05') + def self.datacenter_named(name, client = nil) + datacenters(client).find{ | datacenter | datacenter.name == name.to_s.downcase } + end + + ## + # Return a list of all the datacenters + # + # If the client parameter is not provided, the routine + # will try to use Client::defult_client. If no client + # can be found, the routine will raise an exception + # + # This routine will only retrieve the list of datacenters from + # the network once and keep it in memory unless you + # pass in force_reload as true. + # + @@data_centers = nil + def self.datacenters(client = nil, force_reload = false) + softlayer_client = client || Client.default_client + raise "Datacenter.datacenters requires a client to call the network API" if !softlayer_client + + if(!@@data_centers || force_reload) + datacenters_data = softlayer_client[:Location].getDatacenters + @@data_centers = datacenters_data.collect { | datacenter_data | self.new(softlayer_client, datacenter_data) } + end + + @@data_centers + end + end +end \ No newline at end of file diff --git a/lib/softlayer/DynamicAttribute.rb b/lib/softlayer/DynamicAttribute.rb index 788b2f1..e0ee1cd 100644 --- a/lib/softlayer/DynamicAttribute.rb +++ b/lib/softlayer/DynamicAttribute.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer diff --git a/lib/softlayer/ImageTemplate.rb b/lib/softlayer/ImageTemplate.rb new file mode 100644 index 0000000..4d18073 --- /dev/null +++ b/lib/softlayer/ImageTemplate.rb @@ -0,0 +1,384 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + + +module SoftLayer + ## + # A Virtual Server Image Template. + # + # This class rougly corresponds to the unwieldily named + # +SoftLayer_Virtual_Guest_Block_Device_Template_Group+ + # service: + # + # http://sldn.softlayer.com/reference/services/SoftLayer_Virtual_Guest_Block_Device_Template_Group + # + # + class ImageTemplate < SoftLayer::ModelBase + ## + # :attr_reader: + # The 'friendly name' given to the template when it was created + sl_attr :name + + ## + # :attr_reader: + # The notes, if any, that are attached to the template. Can be nil. + sl_attr :notes, "note" + + ## + # :attr_reader: + # The universally unique identifier (if any) for the template. Can be nil. + sl_attr :global_id, 'globalIdentifier' + + # Change the name of the template + def rename!(new_name) + self.service.editObject({ "name" => new_name.to_s}) + end + + ## + # true if the image template is a flex image + # Note that the publicFlag property comes back as an integer (0 or 1) + def public? + self['publicFlag'] != 0 + end + + ## + # true if the image template is a flex image + # Note that the flexImageFlag property comes back as a boolean + def flex_image? + !!self['flexImageFlag'] + end + + ## + # Changes the notes on an template to be the given strings + def notes=(new_notes) + # it is not a typo that this sets the "note" property. The + # property in the network api is "note", the model exposes it as + # 'notes' for self-consistency + self.service.editObject({ "note" => new_notes.to_s}) + end + + ## + # Returns an array of the tags set on the image + def tags + return self['tagReferences'].collect{ |tag_reference| tag_reference['tag']['name'] } + end + + ## + # Sets the tags on the template. Note: a pre-existing tag will be + # removed from the template if it does not appear in the array given. + # The list of tags must be comprehensive. + def tags=(tags_array) + as_strings = tags_array.collect { |tag| tag.to_s } + self.service.setTags(as_strings.join(',')) + end + + ## + # Returns the an array containing the datacenters where this image is available. + def datacenters + self['datacenters'].collect{ |datacenter_data| SoftLayer::Datacenter.datacenter_named(datacenter_data['name'])} + end + + ## + # Accepts an array of datacenters (instances of SoftLayer::Datacenter) where this + # image should be made available. The call will kick off one or more transactions + # to make the image available in the given datacenters. These transactions can take + # some time to complete. + # + # Note that the template will be REMOVED from any datacenter that does not + # appear in this array! The list given must be comprehensive. + # + # The available_datacenters call returns a list of the values that are valid + # whithin this array. + def datacenters=(datacenters_array) + datacenter_data = datacenters_array.collect do |datacenter| + { "id" => datacenter.id } + end + + self.service.setAvailableLocations(datacenter_data.compact) + end + + ## + # Returns an array of the datacenters that this image can be stored in. + # This is the set of datacenters that you may choose from, when putting + # together a list you will send to the datacenters= setter. + # + def available_datacenters + datacenters_data = self.service.getStorageLocations() + datacenters_data.collect { |datacenter_data| SoftLayer::Datacenter.datacenter_named(datacenter_data['name']) } + end + + + ## + # Returns a list of the accounts (identified by account ID numbers) + # that this image is shared with + def shared_with_accounts + accounts_data = self.service.getAccountReferences + accounts_data.collect { |account_data| account_data['accountId'] } + end + + ## + # Change the set of accounts that this image is shared with. + # The parameter is an array of account ID's. + # + # Note that this routine will "unshare" with any accounts + # not included in the list passed in so the list should + # be comprehensive + # + def shared_with_accounts= (account_id_list) + already_sharing_with = self.shared_with_accounts + + accounts_to_add = account_id_list.select { |account_id| !already_sharing_with.include?(account_id) } + + # Note, using the network API, it is possible to "unshare" an image template + # with the account that owns it, however, this leads to a rather odd state + # where the image has allocated resources (that the account may be charged for) + # but no way to delete those resources. For that reason this model + # always includes the account ID that owns the image in the list of + # accounts the image will be shared with. + my_account_id = self['accountId'] + accounts_to_add.push(my_account_id) if !already_sharing_with.include?(my_account_id) && !accounts_to_add.include?(my_account_id) + + accounts_to_remove = already_sharing_with.select { |account_id| (account_id != my_account_id) && !account_id_list.include?(account_id) } + + accounts_to_add.each {|account_id| self.service.permitSharingAccess account_id } + accounts_to_remove.each {|account_id| self.service.denySharingAccess account_id } + end + + ## + # Creates a transaction to delete the image template and + # all the disk images associated with it. + # + # This is a final action and cannot be undone. + # the transaction will proceed immediately. + # + # Call it with extreme care! + def delete! + self.service.deleteObject + end + + ## + # Repeatedly poll the netwokr API until transactions related to this image + # template are finished + # + # A template is not 'ready' until all the transactions on the template + # itself, and all its children are complete. + # + # At each trial, the routine will yield to a block if one is given + # The block is passed one parameter, a boolean flag indicating + # whether or not the image template is 'ready'. + # + def wait_until_ready(max_trials, seconds_between_tries = 2) + # pessimistically assume the server is not ready + num_trials = 0 + begin + self.refresh_details() + + parent_ready = !(has_sl_property? :transactionId) || (self[:transactionId] == "") + children_ready = (nil == self['children'].find { |child| child['transactionId'] != "" }) + + ready = parent_ready && children_ready + yield ready if block_given? + + num_trials = num_trials + 1 + sleep(seconds_between_tries) if !ready && (num_trials <= max_trials) + end until ready || (num_trials >= max_trials) + + ready + end + + # ModelBase protocol methods + def service + softlayer_client[:Virtual_Guest_Block_Device_Template_Group].object_with_id(self.id) + end + + def softlayer_properties(object_mask = nil) + self.service.object_mask(self.class.default_object_mask).getObject + end + + ## + # Retrieve a list of the private image templates from the account. + # + # The options parameter should contain: + # + # +:client+ - The client used to connect to the API + # + # If no client is given, then the routine will try to use Client.default_client. + # If no client can be found the routine will raise an error. + # + # Additional options that may be provided: + # * +:name+ (string) - Return templates with the given name + # * +:global_id+ (string) - Return templates with the given global identfier + def self.find_private_templates(options_hash = {}) + softlayer_client = options_hash[:client] || Client.default_client + raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client + + if(options_hash.has_key? :object_filter) + object_filter = options_hash[:object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless object_filter.kind_of?(SoftLayer::ObjectFilter) + else + object_filter = ObjectFilter.new() + end + + option_to_filter_path = { + :name => "privateBlockDeviceTemplateGroups.name", + :global_id => "privateBlockDeviceTemplateGroups.globalIdentifier", + } + + # For each of the options in the option_to_filter_path map, if the options hash includes + # that particular option, add a clause to the object filter that filters for the matching + # value + option_to_filter_path.each do |option, filter_path| + object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option])} if options_hash[option] + end + + # Tags get a much more complex object filter operation so we handle them separately + if options_hash.has_key?(:tags) + object_filter.set_criteria_for_key_path("privateBlockDeviceTemplateGroups.tagReferences.tag.name", { + 'operation' => 'in', + 'options' => [{ + 'name' => 'data', + 'value' => options_hash[:tags].collect{ |tag_value| tag_value.to_s } + }] + } ); + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(object_filter) unless object_filter.empty? + account_service = account_service.object_mask(default_object_mask) + + if options_hash.has_key? :object_mask + account_service = account_service.object_mask(options_hash[:object_mask]) + end + + if options_hash.has_key?(:result_limit) + offset = options[:result_limit][:offset] + limit = options[:result_limit][:limit] + + account_service = account_service.result_limit(offset, limit) + end + + templates_data = account_service.getPrivateBlockDeviceTemplateGroups + templates_data.collect { |template_data| new(softlayer_client, template_data) } + end + + ## + # Retrieve a list of public image templates + # + # The options parameter should contain: + # + # +:client+ - The client used to connect to the API + # + # If no client is given, then the routine will try to use Client.default_client + # If no client can be found the routine will raise an error. + # + # Additional options that may be provided: + # * +:name+ (string) - Return templates with the given name + # * +:global_id+ (string) - Return templates with the given global identfier + def self.find_public_templates(options_hash = {}) + softlayer_client = options_hash[:client] || Client.default_client + raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client + + if(options_hash.has_key? :object_filter) + object_filter = options_hash[:object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless object_filter.kind_of?(SoftLayer::ObjectFilter) + else + object_filter = ObjectFilter.new() + end + + option_to_filter_path = { + :name => "publicImages.name", + :global_id => "publicImages.globalIdentifier", + } + + # For each of the options in the option_to_filter_path map, if the options hash includes + # that particular option, add a clause to the object filter that filters for the matching + # value + option_to_filter_path.each do |option, filter_path| + object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option])} if options_hash[option] + end + + # Tags get a much more complex object filter operation so we handle them separately + if options_hash.has_key?(:tags) + object_filter.set_criteria_for_key_path("publicImages.tagReferences.tag.name", { + 'operation' => 'in', + 'options' => [{ + 'name' => 'data', + 'value' => options_hash[:tags].collect{ |tag_value| tag_value.to_s } + }] + } ); + end + + template_service = softlayer_client[:Virtual_Guest_Block_Device_Template_Group] + template_service = template_service.object_filter(object_filter) unless object_filter.empty? + template_service = template_service.object_mask(default_object_mask) + + if options_hash.has_key? :object_mask + template_service = template_service.object_mask(options_hash[:object_mask]) + end + + if options_hash.has_key?(:result_limit) + offset = options[:result_limit][:offset] + limit = options[:result_limit][:limit] + + template_service = template_service.result_limit(offset, limit) + end + + templates_data = template_service.getPublicImages + templates_data.collect { |template_data| new(softlayer_client, template_data) } + end + + ## + # Retrive the Image Template with the given ID + # (Note! This is the service ID, not the globalIdentifier!) + # + # The options parameter should contain: + # + # +:client+ - The client used to connect to the API + # + # If no client is given, then the routine will try to use Client.default_client + # If no client can be found the routine will raise an error. + # + # The options may include the following keys + # * +:object_mask+ (string) - A object mask of properties, in addition to the default properties, that you wish to retrieve for the template + def self.template_with_id(id, options_hash = {}) + softlayer_client = options_hash[:client] || Client.default_client + raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client + + service = softlayer_client[:Virtual_Guest_Block_Device_Template_Group].object_with_id(id) + service.object_mask(default_object_mask) + + if options_hash.has_key? :object_mask + service = service.object_mask(options_hash[:object_mask]) + end + + template_data = service.getObject + new(softlayer_client, template_data) + end + + ## + # Retrieve the image template with the given global ID + # + # The options parameter should contain: + # + # +:client+ - The client used to connect to the API + # + # If no client is given, then the routine will try to use Client.default_client + # If no client can be found the routine will raise an error. + # + # The options may include the following keys + # * +:object_mask+ (string) - A object mask of properties, in addition to the default properties, that you wish to retrieve for the template + def self.template_with_global_id(global_id, options_hash = {}) + templates = find_public_templates(options_hash.merge(:global_id => global_id)) + templates.empty? ? nil : templates[0] + end + + protected + + def self.default_object_mask + return "mask[id,accountId,name,note,globalIdentifier,datacenters,blockDevices,tagReferences,publicFlag,flexImageFlag,transactionId,children.transactionId]" + end + end +end \ No newline at end of file diff --git a/lib/softlayer/ModelBase.rb b/lib/softlayer/ModelBase.rb index d4a499b..5c105f3 100644 --- a/lib/softlayer/ModelBase.rb +++ b/lib/softlayer/ModelBase.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer ## @@ -63,7 +47,7 @@ def initialize(softlayer_client, network_hash) # a particular entity in the SoftLayer_Ticket service. The particular # entity is identified by its id so the Ticket class would return # - # softlayer_client["Ticket"].object_with_id + # softlayer_client[:Ticket].object_with_id # # which is a service which would allow calls to the ticket service # through that particular object. @@ -131,7 +115,7 @@ def to_ary() def softlayer_properties(object_mask = nil) raise "Abstract method softlayer_properties in ModelBase was called" end - + ## # The softlayer_hash stores the low-level information about an # object as it was retrieved from the SoftLayer API. diff --git a/lib/softlayer/NetworkComponent.rb b/lib/softlayer/NetworkComponent.rb new file mode 100644 index 0000000..3d2dbc0 --- /dev/null +++ b/lib/softlayer/NetworkComponent.rb @@ -0,0 +1,14 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + class NetworkComponent < SoftLayer::ModelBase + sl_attr :name + sl_attr :port + sl_attr :speed + sl_attr :maxSpeed + end +end \ No newline at end of file diff --git a/lib/softlayer/ObjectFilter.rb b/lib/softlayer/ObjectFilter.rb index bb22088..791876a 100644 --- a/lib/softlayer/ObjectFilter.rb +++ b/lib/softlayer/ObjectFilter.rb @@ -1,26 +1,102 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer + ## + # An ObjectFilter is a tool that, when passed to the SoftLayer API + # allows the API server to filter, or limit the result set for a call. + # + # Constructing ObjectFilters is an art that is currently somewhat + # arcane. This class tries to simplify filtering for the fundamental + # cases, while still allowing for more complex ObjectFilters to be + # created. + # + # To construct an object filter you begin with an instance of the + # class. At construction time, or in a "modify" call you can change + # the filter criteria using a fancy DSL syntax. + # + # For example, to filter virtual servers so that you only get ones + # whose domains end with "layer.com" you might use: + # + # object_filter = ObjectFilter.new do |filter| + # filter.accept(virtualGuests.domain).when_it ends_with("layer.com") + # end + # + # The set of criteria that can be included after "when_it" are defined + # by routines in the ObjectFilterDefinitionContext module. + class ObjectFilter + def initialize(&construction_block) + @filter_hash = {} + self.modify(&construction_block) + self + end + + def empty? + @filter_hash.empty? + end + + def modify(&construction_block) + ObjectFilterDefinitionContext.module_exec(self, &construction_block) if construction_block + end + + def accept(key_path) + CriteriaAcceptor.new(self, key_path) + end + + def to_h + return @filter_hash.dup + end + + def criteria_for_key_path(key_path) + raise "The key path cannot be empty when searching for criteria" if key_path.nil? || key_path.empty? + + current_level = @filter_hash + keys = key_path.split('.') + + while current_level && keys.count > 1 + current_level = current_level[keys.shift] + end + + if current_level + current_level[keys[0]] + else + nil + end + end + + def set_criteria_for_key_path(key_path, criteria) + current_level = @filter_hash + keys = key_path.split('.') + + current_key = keys.shift + while current_level && !keys.empty? + if !current_level.has_key? current_key + current_level[current_key] = {} + end + current_level = current_level[current_key] + current_key = keys.shift + end + + current_level[current_key] = criteria + end + + class CriteriaAcceptor + def initialize(filter, key_path) + @filter = filter + @key_path = key_path + end + + def when_it(criteria) + @filter.set_criteria_for_key_path(@key_path, criteria) + end + end + end # ObjectFilter + + ## + # :nodoc: OBJECT_FILTER_OPERATORS = [ '*=', # Contains (ignoring case) '^=', # Begins with (ignoring case) @@ -35,192 +111,109 @@ module SoftLayer '!~' # Does not Contain (case sensitive) ] - # A class whose instances represent an Object Filter operator and the value it is applied to. - class ObjectFilterOperation - - # The operator, should be a member of the SoftLayer::OBJECT_FILTER_OPERATORS array - attr_reader :operator - - # The operand of the operator - attr_reader :value - - def initialize(operator, value) - raise ArgumentException, "An unknown operator was given" if !OBJECT_FILTER_OPERATORS.include?(operator.strip) - raise ArgumentException, "Expected a value" if value.nil? || (value.respond_to?(:empty?) && value.empty?) - - @operator = operator.strip - @value = value.strip + ## + # The ObjectFilterDefinitionContext defines a bunch of methods + # that allow the property conditions of an object filter to + # be defined in a "pretty" way. Each method returns a block + # (a lambda, a proc) that, when called and pased the tail property + # of a property chain will generate a fragment of an object filter + # asking that that property match the given conditions. + # + # This class, as a whole, is largely an implementation detail + # of object filter definitions and there is probably not + # a good reason to call into it directly. + module ObjectFilterDefinitionContext + # Matches when the value in the field is exactly equal to the + # given value. This is a case-sensitive match + def self.is(value) + { 'operation' => value } end - def to_h - result = ObjectFilter.new - result['operation'] = "#{operator} #{value}" - - result + # Matches is the value in the field does not exactly equal + # the value passed in. + def self.is_not(value) + filter_criteria('!=', value) end - end - # This class defines the routines that are valid within the block provided to a call to - # ObjectFilter.build. This allows you to create object filters like: - # - # object_filter = SoftLayer::ObjectFilter.build("hardware.memory") { is_greater_than(2) } - # - class ObjectFilterBlockHandler # Matches when the value is found within the field # the search is not case sensitive - def contains(value) - ObjectFilterOperation.new('*=', value) + def self.contains(value) + filter_criteria('*=', value) end # Matches when the value is found at the beginning of the # field. This search is not case sensitive - def begins_with(value) - ObjectFilterOperation.new('^=', value) + def self.begins_with(value) + filter_criteria('^=', value) end # Matches when the value is found at the end of the # field. This search is not case sensitive - def ends_with(value) - ObjectFilterOperation.new('$=', value) + def self.ends_with(value) + filter_criteria('$=', value) end - # Matches when the value in the field is exactly equal to the - # given value. This is a case-sensitive match - def is(value) - ObjectFilterOperation.new('_=', value) - end - - # Matches is the value in the field does not exactly equal - # the value passed in. - def is_not(value) - ObjectFilterOperation.new('!=', value) + # Maches the given value in a case-insensitive way + def self.matches_ignoring_case(value) + filter_criteria('_=', value) end # Matches when the value in the field is greater than the given value - def is_greater_than(value) - ObjectFilterOperation.new('>', value) + def self.is_greater_than(value) + filter_criteria('>', value) end # Matches when the value in the field is less than the given value - def is_less_than(value) - ObjectFilterOperation.new('<', value) + def self.is_less_than(value) + filter_criteria('<', value) end # Matches when the value in the field is greater than or equal to the given value - def is_greater_or_equal_to(value) - ObjectFilterOperation.new('>=', value) + def self.is_greater_or_equal_to(value) + filter_criteria('>=', value) end # Matches when the value in the field is less than or equal to the given value - def is_less_or_equal_to(value) - ObjectFilterOperation.new('<=', value) + def self.is_less_or_equal_to(value) + filter_criteria('<=', value) end # Matches when the value is found within the field # the search _is_ case sensitive - def contains_exactly(value) - ObjectFilterOperation.new('~', value) + def self.contains_exactly(value) + filter_criteria('~', value) end # Matches when the value is not found within the field # the search _is_ case sensitive - def does_not_contain(value) - ObjectFilterOperation.new('!~', value) + def self.does_not_contain(value) + filter_criteria('!~', value) end - end - # - # An ObjectFilter is a tool that, when passed to the SoftLayer API - # allows the API server to filter, or limit the result set for a call. - # - # Constructing ObjectFilters is an art that is currently somewhat - # arcane. This class tries to simplify filtering for the fundamental - # cases, while still allowing for more complex ObjectFilters to be - # created. - # - # The ObjectFilter class is implemented as a hash that, when asked to provide - # an value for an unknown key, will create a sub element - # at that key which is, itself, an object filter. This allows you to build - # up object filters by chaining [] dereference operations. - # - # Starting empty object filter when you ask for +object_filter["foo"]+ - # either the value at that hash location will be returned, or a new +foo+ key - # will be *added* to the object. The value of that key will be an +ObjectFilter+ - # and that +ObjectFilter+ will be returned. - # - # By way of an example of chaining together +[]+ calls: - # object_filter["foo"]["bar"]["baz"] = 3 - # yields an object filter like this: - # {"foo" => { "bar" => {"baz" => 3}}} - # - class ObjectFilter < Hash - # The default initialize for a hash is overridden - # so that object filters create sub-filters when asked - # for missing keys. - def initialize - super do |hash, key| - hash[key] = ObjectFilter.new - end + # Matches when the property's value is null + def self.is_null + { 'operation' => 'is null' } end - # Builds an object filter with the given key path, a dot separated list of property keys. - # The filter itself can be provided as a query string (in the query parameter) - # or by providing a block that calls routines in the ObjectFilterBlockHandler class. - def self.build(key_path, query = nil, &block) - raise ArgumentError, "The key path to build cannot be empty" if !key_path - - # Split the keypath into its constituent parts and notify the user - # if there are no parts - keys = key_path.split('.') - raise ArgumentError, "The key path to build cannot be empty" if keys.empty? - - # This will be the result of the build - result = ObjectFilter.new - - # chase down the key path to the last-but-one key - current_level = result - while keys.count > 1 - current_level = current_level[keys.shift] - end - - # if there is a block, then the query will come from - # calling the block. We warn in debug mode if you override a - # query that was passed directly with the value from a block. - if block - $stderr.puts "The query from the block passed to ObjectFilter:build will override the query passed as a parameter" if $DEBUG && query - block_handler = ObjectFilterBlockHandler.new - query = block_handler.instance_eval(&block) - end - - # If we have a query, we assign its value to the last key - # otherwise, we build an emtpy filter at the bottom - if query - case - when query.kind_of?(Numeric) - current_level[keys.shift] = { 'operation' => query } - when query.kind_of?(SoftLayer::ObjectFilterOperation) - current_level[keys.shift] = query.to_h - when query.kind_of?(String) - current_level[keys.shift] = query_to_filter_operation(query) - when query.kind_of?(Hash) - current_level[keys.shift] = query - else - current_level[keys.shift] - end - else - current_level[keys.shift] - end + # Matches when the property's value is not null + def self.is_not_null() + { 'operation' => 'not null' } + end - result + # This is a catch-all criteria matcher that allows for raw object filter conditions + # not covered by the more convenient methods above. The name is intentionally, annoyingly + # long and you should use this routine with solid knowledge and great care. + def self.satisfies_the_raw_condition(condition_hash) + condition_hash end - # This method simplifies creating correct object filter structures - # by defining a simple query language. It translates strings in that - # language into an Object Filter operations + # Accepts a query string defined by a simple query language. + # It translates strings in that language into criteria blocks # - # Object Filter comparisons are done using operators. Some operators make - # case sensitive comparisons and some do not. The general form of an Object - # Filter operation is an operator follwed by the value used in the comparison. + # Object Filter comparisons can be done using operators. The + # set of accepted operators is found in the OBJECT_FILTER_OPERATORS + # array. The query string can consist of an operator followed + # by a space, followed by operand # e.g. # "*= smaug" # @@ -234,39 +227,48 @@ def self.build(key_path, query = nil, &block) # # This method corresponds to the +query_filter+ method in the SoftLayer-Python # API. - def self.query_to_filter_operation(query) - if query.kind_of? String then - query.strip! + def self.matches_query(query_string) + query = query_string.to_s.strip - begin - return { 'operation' => Integer(query) } - rescue - end - - operator = OBJECT_FILTER_OPERATORS.find do | operator_string | - query[0 ... operator_string.length] == operator_string - end + operator = OBJECT_FILTER_OPERATORS.find do | operator_string | + query[0 ... operator_string.length] == operator_string + end - if operator then - operation = "#{operator} #{query[operator.length..-1].strip}" - else - case query - when /\A\*(.*)\*\Z/ - operation = "*= #{$1}" - when /\A\*(.*)/ - operation = "$= #{$1}" - when /\A(.*)\*\Z/ - operation = "^= #{$1}" - else - operation = "_= #{query}" - end #case - end #if + if operator then + filter_criteria(operator, query[operator.length..-1]) else - operation = query.to_i - end # query is string + case query + when /\A\*(.*)\*\Z/ + contains($1) + when /\A\*(.*)/ + ends_with($1) + when /\A(.*)\*\Z/ + begins_with($1) + else + matches_ignoring_case(query) + end #case + end #if + end - { 'operation' => operation } - end # query_to_filter_operation + private - end # ObjectFilter + def self.cleaned_up_operand(operand) + # try to convert the operand to an integer. If it works, return + # that integer + begin + return Integer(operand) + rescue + end + + # The operand could not be converted to an integer so we try to make it a string + # and clean up the string + filter_operand = operand.to_s.strip + end + + def self.filter_criteria(with_operator, operand) + filter_operand = cleaned_up_operand(operand) + filter_condition = "#{with_operator.to_s.strip} #{operand.to_s.strip}" + { 'operation' => filter_condition } + end + end end # SoftLayer diff --git a/lib/softlayer/ObjectMaskParser.rb b/lib/softlayer/ObjectMaskParser.rb index 5966bd0..4d59929 100644 --- a/lib/softlayer/ObjectMaskParser.rb +++ b/lib/softlayer/ObjectMaskParser.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ require "softlayer/ObjectMaskTokenizer" require "softlayer/ObjectMaskProperty" diff --git a/lib/softlayer/ObjectMaskProperty.rb b/lib/softlayer/ObjectMaskProperty.rb index 6ff0b0f..272c41b 100644 --- a/lib/softlayer/ObjectMaskProperty.rb +++ b/lib/softlayer/ObjectMaskProperty.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer # diff --git a/lib/softlayer/ObjectMaskToken.rb b/lib/softlayer/ObjectMaskToken.rb index ba931e4..854731d 100644 --- a/lib/softlayer/ObjectMaskToken.rb +++ b/lib/softlayer/ObjectMaskToken.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer # diff --git a/lib/softlayer/ObjectMaskTokenizer.rb b/lib/softlayer/ObjectMaskTokenizer.rb index ab7914f..af12096 100644 --- a/lib/softlayer/ObjectMaskTokenizer.rb +++ b/lib/softlayer/ObjectMaskTokenizer.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ require 'softlayer/ObjectMaskToken' require 'strscan' diff --git a/lib/softlayer/ProductItemCategory.rb b/lib/softlayer/ProductItemCategory.rb index b9e925d..7c26b99 100644 --- a/lib/softlayer/ProductItemCategory.rb +++ b/lib/softlayer/ProductItemCategory.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer # This struct represents a configuration option that can be included in @@ -26,10 +10,23 @@ module SoftLayer # the product order is the price_id, the rest of the information is provided # to make the object friendly to humans who may be searching for the # meaning of a given price_id. - ProductConfigurationOption = Struct.new(:price_id, :description, :capacity, :units, :setupFee, :laborFee, :oneTimeFee, :recurringFee, :hourlyRecurringFee) do - + class ProductConfigurationOption < Struct.new(:price_id, :description, :capacity, :units, :setupFee, :laborFee, + :oneTimeFee, :recurringFee, :hourlyRecurringFee) # Is it evil, or just incongruous to give methods to a struct? + def initialize(package_item_data, price_item_data) + self.description = package_item_data['description'] + self.capacity = package_item_data['capacity'] + self.units = package_item_data['units'] + + self.price_id = price_item_data['id'] + self.setupFee = price_item_data['setupFee'] ? price_item_data['setupFee'].to_f : 0.0 + self.laborFee = price_item_data['laborFee'] ? price_item_data['laborFee'].to_f : 0.0 + self.oneTimeFee = price_item_data['oneTimeFee'] ? price_item_data['oneTimeFee'].to_f : 0.0 + self.recurringFee = price_item_data['recurringFee'] ? price_item_data['recurringFee'].to_f : 0.0 + self.hourlyRecurringFee = price_item_data['hourlyRecurringFee'] ? price_item_data['hourlyRecurringFee'].to_f : 0.0 + end + # returns true if the configurtion option has no fees associated with it. def free? self.setupFee == 0 && self.laborFee == 0 && self.oneTimeFee == 0 && self.recurringFee == 0 && self.hourlyRecurringFee == 0 @@ -48,13 +45,13 @@ def free? # service. class ProductItemCategory < ModelBase include ::SoftLayer::DynamicAttribute - + ## # :attr_reader: # The categoryCode is a primary identifier for a particular # category. It is a string like 'os' or 'ram' sl_attr :categoryCode - + ## # :attr_reader: # The name of a category is a friendly, readable string @@ -80,24 +77,14 @@ class ProductItemCategory < ModelBase # web UI), but this code collapses the groups. self['groups'].collect do |group| group['prices'].sort{|lhs,rhs| lhs['sort'] <=> rhs['sort']}.collect do |price_item| - ProductConfigurationOption.new( - price_item['id'], - price_item['item']['description'], - price_item['item']['capacity'], - price_item['item']['units'], - price_item['setupFee'] ? price_item['setupFee'].to_f : 0.0, - price_item['laborFee'] ? price_item['laborFee'].to_f : 0.0, - price_item['oneTimeFee'] ? price_item['oneTimeFee'].to_f : 0.0, - price_item['recurringFee'] ? price_item['recurringFee'].to_f : 0.0, - price_item['hourlyRecurringFee'] ? price_item['hourlyRecurringFee'].to_f : 0.0 - ) + ProductConfigurationOption.new(price_item['item'], price_item) end end.flatten # flatten out the individual group arrays. end end def service - softlayer_client["SoftLayer_Product_Item_Category"].object_with_id(self.id) + softlayer_client[:SoftLayer_Product_Item_Category].object_with_id(self.id) end ## diff --git a/lib/softlayer/ProductPackage.rb b/lib/softlayer/ProductPackage.rb index 1cca71c..58189b4 100644 --- a/lib/softlayer/ProductPackage.rb +++ b/lib/softlayer/ProductPackage.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ require 'json' @@ -59,7 +43,7 @@ class ProductPackage < ModelBase ## # The list of locations where this product package is available. - sl_attr :availableLocations + sl_attr :available_locations, 'availableLocations' ## # The set of product categories needed to make an order for this product package. @@ -81,7 +65,7 @@ class ProductPackage < ModelBase # filtering mechanism on the server side to give us a list of the categories, groups, and prices that are valid for the current # account at the current time. We construct the ProductItemCategory objects from the results we get back. # - configuration_data = softlayer_client['Product_Package'].object_with_id(self.id).object_mask("mask[isRequired,itemCategory.categoryCode]").getConfiguration() + configuration_data = softlayer_client[:Product_Package].object_with_id(self.id).object_mask("mask[isRequired,itemCategory.categoryCode]").getConfiguration() # We sort of invert the information and create a map from category codes to a boolean representing # whether or not they are required. @@ -91,47 +75,82 @@ class ProductPackage < ModelBase end # This call to getCategories is the one that does lots of fancy back-end filtering for us - categories_data = softlayer_client['Product_Package'].object_with_id(self.id).getCategories() + categories_data = softlayer_client[:Product_Package].object_with_id(self.id).getCategories() # Run though the categories and for each one that's in our config, create a SoftLayer::ProductItemCategory object. # Conveniently the +keys+ of the required_by_category_code gives us a list of the category codes in the configuration config_categories = required_by_category_code.keys - categories_data.collect do |category_data| + + # collect all the categories into an array + @categories = categories_data.collect do |category_data| if config_categories.include? category_data['categoryCode'] SoftLayer::ProductItemCategory.new(softlayer_client, category_data, required_by_category_code[category_data['categoryCode']]) else - nil + SoftLayer::ProductItemCategory.new(softlayer_client, category_data, false) end end.compact + + # The configuration consists of only those categories that are required. + @categories.select { |category| category.required? } + end # to_update + end # configuration + + ## + # The full set of product categories contained in the package + # + sl_dynamic_attr :categories do |resource| + resource.should_update? do + @categories == nil + end + + resource.to_update do + # This is a bit ugly, but what we do is ask for the configuration + # which updates all the categories for the package (and marks those + # that are required) + self.configuration + + # return the value constructed by the configuraiton + @categories end end ## # Returns an array of the required categories in this package def required_categories - configuration.select { |category| category.required? } + configuration end ## # Returns the product category with the given category code (or nil if one cannot be found) def category(category_code) - configuration.find { |category| category.categoryCode == category_code } + categories.find { |category| category.categoryCode == category_code } end + ## + # Returns a list of the datacenters that this package is available in def datacenter_options - availableLocations.collect { |location_data| location_data["location"]["name"] } + available_locations.collect { |location_data| Datacenter::datacenter_named(location_data['location']['name'], self.softlayer_client) }.compact end ## - # Given a datacenter name that was returned by datacenter_options, use information - # in the package to retrieve a location id. - def location_id_for_datacenter_name(datacenter_name) - location_data = availableLocations.find { |location_data| location_data["location"]["name"] == datacenter_name } - location_data["locationId"] + # Returns the package items with the given description + # Currently this is returning the low-level hash representation directly from the Network API + # + def items_with_description(expected_description) + filter = ObjectFilter.new { |filter| filter.accept("items.description").when_it is(expected_description) } + items_data = self.service.object_filter(filter).getItems() + + items_data.collect do |item_data| + first_price = item_data['prices'][0] + ProductConfigurationOption.new(item_data, first_price) + end end + ## + # Returns the service for interacting with this package through the network API + # def service - softlayer_client['Product_Package'].object_with_id(self.id) + softlayer_client[:Product_Package].object_with_id(self.id) end ## @@ -141,9 +160,12 @@ def service def self.packages_with_key_name(key_name, client = nil) softlayer_client = client || Client.default_client raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - - filter = SoftLayer::ObjectFilter.build('type.keyName', key_name) - filtered_service = softlayer_client['Product_Package'].object_filter(filter).object_mask(self.default_object_mask('mask')) + + filter = SoftLayer::ObjectFilter.new do |filter| + filter.accept('type.keyName').when_it is(key_name) + end + + filtered_service = softlayer_client[:Product_Package].object_filter(filter).object_mask(self.default_object_mask('mask')) packages_data = filtered_service.getAllObjects packages_data.collect { |package_data| ProductPackage.new(softlayer_client, package_data) } end @@ -155,8 +177,8 @@ def self.packages_with_key_name(key_name, client = nil) def self.package_with_id(package_id, client = nil) softlayer_client = client || Client.default_client raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - - package_data = softlayer_client['Product_Package'].object_with_id(package_id).object_mask(self.default_object_mask('mask')).getObject + + package_data = softlayer_client[:Product_Package].object_with_id(package_id).object_mask(self.default_object_mask('mask')).getObject ProductPackage.new(softlayer_client, package_data) end @@ -189,6 +211,13 @@ def self.bare_metal_server_packages(client = nil) packages_with_key_name('BARE_METAL_CPU', client) end + ## + # The "Additional Products" package is a grab-bag of products + # and services. It has a "well known" id of 0 + def self.additional_products_package(client = nil) + return package_with_id(0, client) + end + protected def self.default_object_mask(root) diff --git a/lib/softlayer/Server.rb b/lib/softlayer/Server.rb index cb1737b..a75b2c6 100644 --- a/lib/softlayer/Server.rb +++ b/lib/softlayer/Server.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer # Server is the base class for VirtualServer and BareMetalServer. @@ -32,6 +16,7 @@ module SoftLayer # ancestry. As a result there is no SoftLayer API analog # to this class. class Server < SoftLayer::ModelBase + include ::SoftLayer::DynamicAttribute ## # :attr_reader: @@ -68,6 +53,17 @@ class Server < SoftLayer::ModelBase # Notes about these server (for use by the customer) sl_attr :notes + sl_dynamic_attr :primary_network_component do |primary_component| + primary_component.should_update? do + return @primary_network_component == nil + end + + primary_component.to_update do + component_data = self.service.getPrimaryNetworkComponent(); + SoftLayer::NetworkComponent.new(self.softlayer_client, component_data) + end + end + ## # Construct a server from the given client using the network data found in +network_hash+ # @@ -83,6 +79,25 @@ def initialize(softlayer_client, network_hash) end end + ## + # Reboot the server. This action is taken immediately. + # Servers can be rebooted in three different ways: + # :default_reboot - (Try soft, then hard) Attempts to reboot a server using the :os_reboot technique then, if that is not successful, tries the :power_cycle method + # :os_reboot - (aka. soft rebot) instructs the server's host operating system to reboot + # :power_cycle - (aka. hard reboot) The actual (for hardware) or metaphorical (for virtual servers) equivalent to pulling the plug on the server then plugging it back in. + def reboot!(reboot_technique = :default_reboot) + case reboot_technique + when :default_reboot + self.service.rebootDefault + when :os_reboot + self.service.rebootSoft + when :power_cycle + self.service.rebootHard + else + raise ArgumentError, "Unrecognized reboot technique in SoftLayer::Server#reboot!}" + end + end + ## # Make an API request to SoftLayer and return the latest properties hash # for this object. @@ -102,7 +117,7 @@ def softlayer_properties(object_mask = nil) # Change the notes of the server # raises ArgumentError if you pass nil as the notes def notes=(new_notes) - raise ArgumentError.new("The new notes cannot be nil") unless new_notes + raise ArgumentError, "The new notes cannot be nil" unless new_notes edit_template = { "notes" => new_notes @@ -116,7 +131,7 @@ def notes=(new_notes) # Change the user metadata for the server. # def user_metadata=(new_metadata) - raise ArgumentError.new("Cannot set user metadata to nil") unless new_metadata + raise ArgumentError, "Cannot set user metadata to nil" unless new_metadata self.service.setUserMetadata([new_metadata]) self.refresh_details() end @@ -126,8 +141,8 @@ def user_metadata=(new_metadata) # Raises an ArgumentError if the new hostname is nil or empty # def set_hostname!(new_hostname) - raise ArgumentError.new("The new hostname cannot be nil") unless new_hostname - raise ArgumentError.new("The new hostname cannot be empty") if new_hostname.empty? + raise ArgumentError, "The new hostname cannot be nil" unless new_hostname + raise ArgumentError, "The new hostname cannot be empty" if new_hostname.empty? edit_template = { "hostname" => new_hostname @@ -144,8 +159,8 @@ def set_hostname!(new_hostname) # no further validation is done on the domain name # def set_domain!(new_domain) - raise ArgumentError.new("The new hostname cannot be nil") unless new_domain - raise ArgumentError.new("The new hostname cannot be empty") if new_domain.empty? + raise ArgumentError, "The new hostname cannot be nil" unless new_domain + raise ArgumentError, "The new hostname cannot be empty" if new_domain.empty? edit_template = { "domain" => new_domain @@ -155,6 +170,16 @@ def set_domain!(new_domain) self.refresh_details() end + ## + # Returns the max port speed of the public network interfaces of the server taking into account + # bound interface pairs (redundant network cards). + def firewall_port_speed + network_components = self.service.object_mask("mask[id,maxSpeed]").getFrontendNetworkComponents() + max_speeds = network_components.collect { |component| component['maxSpeed'] } + + max_speeds.empty? ? 0 : max_speeds.max + end + ## # Change the current port speed of the server # @@ -163,8 +188,7 @@ def set_domain!(new_domain) # on the port. # # Set +public+ to +false+ in order to change the speed of the - # primary private network interface. - # + # private network interface. def change_port_speed(new_speed, public = true) if public self.service.setPublicNetworkInterfaceSpeed(new_speed) diff --git a/lib/softlayer/ServerFirewall.rb b/lib/softlayer/ServerFirewall.rb new file mode 100644 index 0000000..0fcf9f2 --- /dev/null +++ b/lib/softlayer/ServerFirewall.rb @@ -0,0 +1,263 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # The ServerFirewall class represents a firewall in the + # SoftLayer environment that exists in a 1 to 1 relationship + # with a particular server (either Bare Metal or Virtual). + # + # This is also called a "Shared Firewall" in some documentation. + # + # Instances of this class rougly correspond to instances of the + # SoftLayer_Network_Component_Firewall service entity. + # + class ServerFirewall < SoftLayer::ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: + # The state of the firewall, includes whether or not the rules are + # editable and whether or not the firewall rules are applied or bypassed + # Can at least be 'allow_edit', 'bypass' or 'no_edit'. + # This list may not be exhaustive + sl_attr :status + + ## + # :attr_reader: + # The firewall rules assigned to this firewall. These rules will + # be read from the network API every time you ask for the value + # of this property. To change the rules on the server use the + # asymmetric method change_rules! + sl_dynamic_attr :rules do |firewall_rules| + firewall_rules.should_update? do + # firewall rules update every time you ask for them. + return true + end + + firewall_rules.to_update do + rules_data = self.service.object_mask(self.class.default_rules_mask).getRules() + + # At the time of this writing, the object mask sent to getRules is not + # applied properly. This has been reported as a bug to the proper + # development team. In the mean time, this extra step does filtering + # that should have been done by the object mask. + rules_keys = self.class.default_rules_mask_keys + new_rules = rules_data.inject([]) do |new_rules, current_rule| + new_rule = current_rule.delete_if { |key, value| !(rules_keys.include? key) } + new_rules << new_rule + end + + new_rules.sort { |lhs, rhs| lhs['orderValue'] <=> rhs['orderValue'] } + end + end + + ## + # :attr_reader: + # The server that this firewall is attached to. The result may be + # either a bare metal or virtual server. + # + sl_dynamic_attr :protected_server do |protected_server| + protected_server.should_update? do + @protected_server == nil + end + + protected_server.to_update do + if has_sl_property?('networkComponent') + @protected_server = SoftLayer::BareMetalServer.server_with_id(self['networkComponent']['downlinkComponent']['hardwareId'], :client => softlayer_client) + end + + if has_sl_property?('guestNetworkComponent') + @protected_server = SoftLayer::VirtualServer.server_with_id(self['guestNetworkComponent']['guest']['id'], :client => softlayer_client) + end + + @protected_server + end + end + + ## + # Calls super to initialize the object then initializes some + # properties + def initialize(client, network_hash) + super(client, network_hash) + @protected_server = nil + end + + ## + # Cancel the firewall + # + # This method cancels the firewall and releases its + # resources. The cancellation is processed immediately! + # Call this method with careful deliberation! + # + # Notes is a string that describes the reason for the + # cancellation. If empty or nil, a default string will + # be added + # + def cancel!(notes = nil) + user = self.softlayer_client[:Account].object_mask("mask[id,account]").getCurrentUser + notes = "Cancelled by a call to #{__method__} in the softlayer_api gem" if notes == nil || notes == "" + + cancellation_request = { + 'accountId' => user['account']['id'], + 'userId' => user['id'], + 'items' => [ { + 'billingItemId' => self['billingItem']['id'], + 'immediateCancellationFlag' => true + } ], + 'notes' => notes + } + + self.softlayer_client[:Billing_Item_Cancellation_Request].createObject(cancellation_request) + end + + ## + # Change the set of rules for the firewall. + # The rules_data parameter should be an array of hashes where + # each hash gives the conditions of the rule. The keys of the + # hashes should be entries from the array returned by + # SoftLayer::ServerFirewall.default_rules_mask_keys + # + # *NOTE!* When changing the rules on the firewall, you must + # pass in a complete set of rules each time. The rules you + # submit will replace the entire ruleset on the destination + # firewall. + # + # *NOTE!* The rules themselves have an "orderValue" property. + # It is this property, and *not* the order that the rules are + # found in the rules_data array, which will determine in which + # order the firewall applies it's rules to incomming traffic. + # + # *NOTE!* Changes to the rules are not applied immediately + # on the server side. Instead, they are enqueued by the + # firewall update service and updated periodically. A typical + # update will take about one minute to apply, but times may vary + # depending on the system load and other circumstances. + def change_rules!(rules_data) + change_object = { + "networkComponentFirewallId" => self.id, + "rules" => rules_data + } + + self.softlayer_client[:Network_Firewall_Update_Request].createObject(change_object) + end + + ## + # This method asks the firewall to ignore its rule set and pass all traffic + # through the firewall. Compare the behavior of this routine with + # change_routing_bypass! + # + # It is important to note that changing the bypass to :bypass_firewall_rules + # removes ALL the protection offered by the firewall. This routine should be + # used with careful deliberation. + # + # Note that this routine queues a rule change and rule changes may take + # time to process. The change will probably not take effect immediately. + # + # The two symbols accepted as arguments by this routine are: + # :apply_firewall_rules - The rules of the firewall are applied to traffic. This is the default operating mode of the firewall + # :bypass_firewall_rules - The rules of the firewall are ignored. In this configuration the firewall provides no protection. + # + def change_rules_bypass!(bypass_symbol) + change_object = { + "networkComponentFirewallId" => self.id, + "rules" => self.rules + } + + case bypass_symbol + when :apply_firewall_rules + change_object['bypassFlag'] = false + self.softlayer_client[:Network_Firewall_Update_Request].createObject(change_object) + when :bypass_firewall_rules + change_object['bypassFlag'] = true + self.softlayer_client[:Network_Firewall_Update_Request].createObject(change_object) + else + raise ArgumentError, "An invalid parameter was sent to #{__method__}. It accepts :apply_firewall_rules and :bypass_firewall_rules" + end + end + + ## + # Locate and return all the server firewalls in the environment. + # + # These are a bit tricky to track down. The strategy we take here is + # to look at the account and find all the VLANs that do NOT have their + # "dedicatedFirewallFlag" set. + # + # With the list of VLANs in hand we check each to see if it has an + # firewallNetworkComponents (corresponding to bare metal servers) or + # firewallGuestNetworkComponents (corresponding to virtual servers) that + # have a status of "allow_edit". Each such component is a firewall + # interface on the VLAN with rules that the customer can edit. + # + # The collection of all those VLANs becomes the set of firewalls + # for the account. + # + def self.find_firewalls(client = nil) + softlayer_client = client || Client.default_client + raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client + + # Note that the dedicatedFirewallFlag is actually an integer and not a boolean + # so we compare it against 0 + shared_vlans_filter = SoftLayer::ObjectFilter.new() { |filter| + filter.accept("networkVlans.dedicatedFirewallFlag").when_it is(0) + } + + bare_metal_firewalls_data = [] + virtual_firewalls_data = [] + + shared_vlans = softlayer_client[:Account].object_mask(network_vlan_mask).object_filter(shared_vlans_filter).getNetworkVlans + shared_vlans.each do |vlan_data| + bare_metal_firewalls_data.concat vlan_data['firewallNetworkComponents'].select { |network_component| network_component['status'] != 'no_edit'} + virtual_firewalls_data.concat vlan_data['firewallGuestNetworkComponents'].select { |network_component| network_component['status'] != 'no_edit'} + end + + bare_metal_firewalls = bare_metal_firewalls_data.collect { |bare_metal_firewall_data| + self.new(softlayer_client, bare_metal_firewall_data) + } + + virtual_server_firewalls = virtual_firewalls_data.collect { |virtual_firewall_data| + self.new(softlayer_client, virtual_firewall_data) + } + + return bare_metal_firewalls + virtual_server_firewalls + end + + #-- + # Methods for the SoftLayer model + #++ + + def service + self.softlayer_client[:Network_Component_Firewall].object_with_id(self.id) + end + + def softlayer_properties(object_mask = nil) + service = self.service + service = service.object_mask(object_mask) if object_mask + + if self.has_sl_property?('networkComponent') + service.object_mask("mask[id,status,billingItem.id,networkComponent.downlinkComponent.hardwareId]").getObject + else + service.object_mask("mask[id,status,billingItem.id,guestNetworkComponent.guest.id]").getObject + end + end + + #-- + #++ + private + + def self.network_vlan_mask + "mask[firewallNetworkComponents[id,status,billingItem.id,networkComponent.downlinkComponent.hardwareId],firewallGuestNetworkComponents[id,status,billingItem.id,guestNetworkComponent.guest.id]]" + end + + def self.default_rules_mask + return { "mask" => default_rules_mask_keys }.to_sl_object_mask + end + + def self.default_rules_mask_keys + ['orderValue','action','destinationIpAddress','destinationIpSubnetMask',"protocol","destinationPortRangeStart","destinationPortRangeEnd",'sourceIpAddress',"sourceIpSubnetMask","version"] + end + end # ServerFirewall class +end # SoftLayer module \ No newline at end of file diff --git a/lib/softlayer/ServerFirewallOrder.rb b/lib/softlayer/ServerFirewallOrder.rb new file mode 100644 index 0000000..202f174 --- /dev/null +++ b/lib/softlayer/ServerFirewallOrder.rb @@ -0,0 +1,84 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + # + # This class allows you to order a Firewall for a server + # + class ServerFirewallOrder + # The server that you are ordering the firewall for. + attr_reader :server + + ## + # Create a new order for the given server + def initialize (server) + @server = server + + raise ArgumentError, "Server does not have an active Public interface" if server.firewall_port_speed == 0 + end + + ## + # Calls the SoftLayer API to verify that the template provided by this order is valid + # This routine will return the order template generated by the API or will throw an exception + # + # This routine will not actually create a Bare Metal Instance and will not affect billing. + # + # If you provide a block, it will receive the order template as a parameter and + # the block may make changes to the template before it is submitted. + def verify() + order_template = firewall_order_template + order_template = yield order_template if block_given? + + server.softlayer_client[:Product_Order].verifyOrder(order_template) + end + + ## + # Calls the SoftLayer API to place an order for a new server based on the template in this + # order. If this succeeds then you will be billed for the new server. + # + # If you provide a block, it will receive the order template as a parameter and + # the block may make changes to the template before it is submitted. + def place_order!() + order_template = firewall_order_template + order_template = yield order_template if block_given? + + server.softlayer_client[:Product_Order].placeOrder(order_template) + end + + protected + + ## + # Returns a hash of the creation options formatted to be sent *to* + # the SoftLayer API for either verification or completion + def firewall_order_template + client = server.softlayer_client + additional_products_package = SoftLayer::ProductPackage.additional_products_package(client) + + template = { + 'complexType' => 'SoftLayer_Container_Product_Order_Network_Protection_Firewall', + 'quantity' => 1, + 'packageId' => additional_products_package.id + } + + if @server.service.service_name == "SoftLayer_Virtual_Guest" + template['virtualGuests'] = [{'id' => @server.id}] + else + template['hardware'] = [{'id' => @server.id}] + end + + expected_description = "#{@server.firewall_port_speed}Mbps Hardware Firewall" + firewall_items = additional_products_package.items_with_description(expected_description) + + raise "Could not find a price item matching the description '#{expected_description}'" if firewall_items.empty? + + firewall_item = firewall_items[0] + + template['prices'] = [{ 'id' => firewall_item.price_id }] if firewall_item.respond_to?(:price_id) + + template + end + end # class ServerFirewallOrder +end # module SoftLayer diff --git a/lib/softlayer/Service.rb b/lib/softlayer/Service.rb index 51145d4..6e20664 100644 --- a/lib/softlayer/Service.rb +++ b/lib/softlayer/Service.rb @@ -20,6 +20,19 @@ require 'xmlrpc/client' +# utility routine for swapping constants without warnings. +def with_warnings(flag) + old_verbose, $VERBOSE = $VERBOSE, flag + yield +ensure + $VERBOSE = old_verbose +end + +# enable parsing of "nil" values in structures returned from the API +with_warnings(nil) { + XMLRPC::Config.const_set('ENABLE_NIL_PARSER', true) +} + # The XML-RPC spec calls for the "faultCode" in faults to be an integer # but the SoftLayer XML-RPC API can return strings as the "faultCode" # @@ -30,25 +43,15 @@ module XMLRPC::Convert def self.fault(hash) if hash.kind_of? Hash and hash.size == 2 and hash.has_key? "faultCode" and hash.has_key? "faultString" and - (hash["faultCode"].kind_of?(Integer) || hash["faultCode"].kind_of?(String)) and hash["faultString"].kind_of? String + (hash['faultCode'].kind_of?(Integer) || hash['faultCode'].kind_of?(String)) and hash['faultString'].kind_of? String - XMLRPC::FaultException.new(hash["faultCode"], hash["faultString"]) + XMLRPC::FaultException.new(hash['faultCode'], hash['faultString']) else super end end end -# The XMLRPC client uses a fixed user agent string, but we want to -# supply our own, so we add a method to XMLRPC::Client that lets -# us change it. -class XMLRPC::Client - def self.set_user_agent(new_agent) - remove_const(:USER_AGENT) if const_defined?(:USER_AGENT) - const_set(:USER_AGENT, new_agent) - end -end - module SoftLayer # = SoftLayer API Service # @@ -62,7 +65,7 @@ module SoftLayer # # client = SoftLayer::Client.new(:username => "Joe", :api_key=>"feeddeadbeefbadfood...") # account_service = client.service_named("Account") # returns the SoftLayer_Account service - # account_service = client['Account'] # Exactly the same as above + # account_service = client[:Account] # Exactly the same as above # # For backward compatibility, a service can be constructed by passing # client initialization options, however if you do so you will need to @@ -315,6 +318,7 @@ def http end @xmlrpc_client.http.set_debug_output($stderr) + @xmlrpc_client.http.instance_variable_set(:@verify_mode, OpenSSL::SSL::VERIFY_NONE) end # $DEBUG end diff --git a/lib/softlayer/Ticket.rb b/lib/softlayer/Ticket.rb index c7a9a05..080bef8 100644 --- a/lib/softlayer/Ticket.rb +++ b/lib/softlayer/Ticket.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer class Ticket < SoftLayer::ModelBase @@ -32,23 +16,23 @@ class Ticket < SoftLayer::ModelBase # :attr_reader: # The ticket system maintains a fixed set of subjects for tickets that are used to ensure tickets make it to the right folks quickly sl_attr :subject - + ## # :attr_reader: - # The date the ticket was last updated. + # The date the ticket was last updated. sl_attr :lastEditDate ## # Returns true if the ticket has "unread" updates def has_updates? - self["newUpdatesFlag"] + self['newUpdatesFlag'] end ## # Returns true if the ticket is a server admin ticket def server_admin_ticket? # note that serverAdministrationFlag comes from the server as an Integer (0, or 1) - self["serverAdministrationFlag"] != 0 + self['serverAdministrationFlag'] != 0 end ## @@ -62,7 +46,7 @@ def update(body = nil) # Override of service from ModelBase. Returns the SoftLayer_Ticket service # set up to talk to the ticket with my ID. def service - return softlayer_client["Ticket"].object_with_id(self.id) + return softlayer_client[:Ticket].object_with_id(self.id) end ## @@ -99,7 +83,7 @@ def self.default_object_mask 'awaitingUserResponseFlag', # This comes in from the server as a Boolean value 'serverAdministrationFlag', # This comes in from the server as an integer :-( ] - } + }.to_sl_object_mask end ## @@ -112,35 +96,12 @@ def self.ticket_subjects(client = nil) softlayer_client = client || Client.default_client raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - @ticket_subjects = softlayer_client['Ticket_Subject'].getAllObjects(); + @ticket_subjects = softlayer_client[:Ticket_Subject].getAllObjects(); end @ticket_subjects end - ## - # Returns the set of currently open tickets - # - # Options should contain: - # - # +:client+ - the client in which to search for the ticket - # - # If a client is not provided then the routine will search Client::default_client - # If Client::default_client is also nil the routine will raise an error. - def self.open_tickets(options = {}) - softlayer_client = options[:client] || Client.default_client - raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - - if options.has_key?(:object_mask) - object_mask = options[:object_mask] - else - object_mask = default_object_mask.to_sl_object_mask - end - - open_tickets_data = softlayer_client["Account"].object_mask(object_mask).getOpenTickets - open_tickets_data.collect { |ticket_data| new(softlayer_client, ticket_data) } - end - ## # Find the ticket with the given ID and return it # @@ -161,7 +122,7 @@ def self.ticket_with_id(ticket_id, options = {}) object_mask = default_object_mask.to_sl_object_mask end - ticket_data = softlayer_client["Ticket"].object_with_id(ticket_id).object_mask(object_mask).getObject() + ticket_data = softlayer_client[:Ticket].object_with_id(ticket_id).object_mask(object_mask).getObject() return new(softlayer_client, ticket_data) end @@ -192,8 +153,8 @@ def self.create_standard_ticket(options = {}) assigned_user_id = options[:assigned_user_id] if(nil == assigned_user_id) - current_user = softlayer_client["Account"].object_mask("id").getCurrentUser() - assigned_user_id = current_user["id"] + current_user = softlayer_client[:Account].object_mask("id").getCurrentUser() + assigned_user_id = current_user['id'] end new_ticket = { @@ -203,7 +164,7 @@ def self.create_standard_ticket(options = {}) 'title' => title } - ticket_data = softlayer_client["Ticket"].createStandardTicket(new_ticket, body) + ticket_data = softlayer_client[:Ticket].createStandardTicket(new_ticket, body) return new(softlayer_client, ticket_data) end end diff --git a/lib/softlayer/VLANFirewall.rb b/lib/softlayer/VLANFirewall.rb new file mode 100644 index 0000000..311f37f --- /dev/null +++ b/lib/softlayer/VLANFirewall.rb @@ -0,0 +1,280 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + # The VLANFirewall class represents the firewall that protects + # all the servers on a VLAN in the SoftLayer Environment. It is + # also known as a "Dedicated Firewall" in some documentation. + # + # Instances of this class are a bit odd because they actually represent a + # VLAN (the VLAN protected by the firewall to be specific), and not the + # physical hardware implementing the firewall itself. (although the device + # is accessible as the "networkVlanFirewall" property) + # + # As a result, instances of this class correspond to certain instances + # in the SoftLayer_Network_Vlan service. + # + class VLANFirewall < SoftLayer::ModelBase + include ::SoftLayer::DynamicAttribute + + ## + #:attr_reader: + # + # The number of the VLAN protected by this firewall. + # + sl_attr :VLAN_number, 'vlanNumber' + + ## + # :attr_reader: + # + # The set of rules applied by this firewall to incoming traffic. + # The object will retrieve the rules from the network API every + # time you ask it for the rules. + # + # The code will sort the rules by their "orderValue" which is the + # order that the firewall applies the rules, however please see + # the important note in change_rules! concerning the "orderValue" + # property of the rules. + sl_dynamic_attr :rules do |firewall_rules| + firewall_rules.should_update? do + # firewall rules update every time you ask for them. + return true + end + + firewall_rules.to_update do + acl_id = rules_ACL_id() + rules_data = self.softlayer_client[:Network_Firewall_AccessControlList].object_with_id(acl_id).object_mask(self.class.default_rules_mask).getRules + rules_data.sort { |lhs, rhs| lhs['orderValue'] <=> rhs['orderValue'] } + end + end + + ## + # Returns the name of the primary router the firewall is attached to. + # This is often a "customer router" in one of the datacenters. + def primaryRouter + return self['primaryRouter']['hostname'] + end + + ## + # The fully qualified domain name of the physical device the + # firewall is implemented by. + def fullyQualifiedDomainName + if self.has_sl_property?('networkVlanFirewall') + return self['networkVlanFirewall']['fullyQualifiedDomainName'] + else + return @softlayer_hash + end + end + + ## + # Returns true if this is a "high availability" firewall, that is a firewall + # that exists as one member of a redundant pair. + def high_availability? + # note that highAvailabilityFirewallFlag is a boolean in the softlayer hash + return self.has_sl_property?('highAvailabilityFirewallFlag') && self['highAvailabilityFirewallFlag'] + end + + ## + # Cancel the firewall + # + # This method cancels the firewall and releases its + # resources. The cancellation is processed immediately! + # Call this method with careful deliberation! + # + # Notes is a string that describes the reason for the + # cancellation. If empty or nil, a default string will + # be added. + # + def cancel!(notes = nil) + user = self.softlayer_client[:Account].object_mask("mask[id,account.id]").getCurrentUser + notes = "Cancelled by a call to #{__method__} in the softlayer_api gem" if notes == nil || notes == "" + + cancellation_request = { + 'accountId' => user['account']['id'], + 'userId' => user['id'], + 'items' => [ { + 'billingItemId' => self['networkVlanFirewall']['billingItem']['id'], + 'immediateCancellationFlag' => true + } ], + 'notes' => notes + } + + self.softlayer_client[:Billing_Item_Cancellation_Request].createObject(cancellation_request) + end + + ## + # Change the set of rules for the firewall. + # The rules_data parameter should be an array of hashes where + # each hash gives the conditions of the rule. The keys of the + # hashes should be entries from the array returned by + # SoftLayer::ServerFirewall.default_rules_mask_keys + # + # *NOTE!* When changing the rules on the firewall, you must + # pass in a complete set of rules each time. The rules you + # submit will replace the entire ruleset on the destination + # firewall. + # + # *NOTE!* The rules themselves have an "orderValue" property. + # It is this property, and *not* the order that the rules are + # found in the rules_data array, which will determine in which + # order the firewall applies its rules to incomming traffic. + # + # *NOTE!* Changes to the rules are not applied immediately + # on the server side. Instead, they are enqueued by the + # firewall update service and updated periodically. A typical + # update will take about one minute to apply, but times may vary + # depending on the system load and other circumstances. + def change_rules!(rules_data) + change_object = { + "firewallContextAccessControlListId" => rules_ACL_id(), + "rules" => rules_data + } + + self.softlayer_client[:Network_Firewall_Update_Request].createObject(change_object) + end + + ## + # This method asks the firewall to ignore its rule set and pass all traffic + # through the firewall. Compare the behavior of this routine with + # change_routing_bypass! + # + # It is important to note that changing the bypass to :bypass_firewall_rules + # removes ALL the protection offered by the firewall. This routine should be + # used with extreme discretion. + # + # Note that this routine queues a rule change and rule changes may take + # time to process. The change will probably not take effect immediately. + # + # The two symbols accepted as arguments by this routine are: + # :apply_firewall_rules - The rules of the firewall are applied to traffic. This is the default operating mode of the firewall + # :bypass_firewall_rules - The rules of the firewall are ignored. In this configuration the firewall provides no protection. + # + def change_rules_bypass!(bypass_symbol) + change_object = { + "firewallContextAccessControlListId" => rules_ACL_id(), + "rules" => self.rules + } + + case bypass_symbol + when :apply_firewall_rules + change_object['bypassFlag'] = false + self.softlayer_client[:Network_Firewall_Update_Request].createObject(change_object) + when :bypass_firewall_rules + change_object['bypassFlag'] = true + self.softlayer_client[:Network_Firewall_Update_Request].createObject(change_object) + else + raise ArgumentError, "An invalid parameter was sent to #{__method__}. It accepts :apply_firewall_rules and :bypass_firewall_rules" + end + end + + ## + # This method allows you to route traffic around the firewall + # and directly to the servers it protects. Compare the behavior of this routine with + # change_rules_bypass! + # + # It is important to note that changing the routing to :route_around_firewall + # removes ALL the protection offered by the firewall. This routine should be + # used with extreme discretion. + # + # Note that this routine constructs a transaction. The Routing change + # may not happen immediately. + # + # The two symbols accepted as arguments by the routine are: + # :route_through_firewall - Network traffic is sent through the firewall to the servers in the VLAN segment it protects. This is the usual operating mode of the firewall. + # :route_around_firewall - Network traffic will be sent directly to the servers in the VLAN segment protected by this firewall. This means that the firewall will *NOT* be protecting those servers. + # + def change_routing_bypass!(routing_symbol) + vlan_firewall_id = self['networkVlanFirewall']['id'] + + raise "Could not identify the device for a VLAN firewall" if !vlan_firewall_id + + case routing_symbol + when :route_through_firewall + self.softlayer_client[:Network_Vlan_Firewall].object_with_id(vlan_firewall_id).updateRouteBypass(false) + when :route_around_firewall + self.softlayer_client[:Network_Vlan_Firewall].object_with_id(vlan_firewall_id).updateRouteBypass(true) + else + raise ArgumentError, "An invalid parameter was sent to #{__method__}. It accepts :route_through_firewall and :route_around_firewall" + end + end + + ## + # Collect a list of the firewalls on the account. + # + # This list is obtained by asking the account for all the VLANs + # it has that also have a networkVlanFirewall component. + def self.find_firewalls(client = nil) + softlayer_client = client || Client.default_client + raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client + + # only VLAN firewallas have a networkVlanFirewall component + vlan_firewall_filter = SoftLayer::ObjectFilter.new() { |filter| + filter.accept("networkVlans.networkVlanFirewall").when_it is_not_null + } + + vlan_firewalls = softlayer_client[:Account].object_mask(vlan_firewall_mask).object_filter(vlan_firewall_filter).getNetworkVlans + vlan_firewalls.collect { |firewall_data| SoftLayer::VLANFirewall.new(softlayer_client, firewall_data)} + end + + + #-- + # Methods for the SoftLayer model + #++ + + def service + # Objects of this class are a bit odd because they actually represent VLANs (the VLAN protected by the firewall) + # and not the physical hardware implementing the firewall itself. (although the device is accessible as the + # "networkVlanFirewall" property) + self.softlayer_client[:Network_Vlan].object_with_id(self.id) + end + + def softlayer_properties(object_mask = nil) + service = self.service + service = service.object_mask(object_mask) if object_mask + service.object_mask(self.class.vlan_firewall_mask).getObject + end + + #-- + #++ + private + + # Searches the set of access control lists for the firewall device in order to locate the one that + # sits on the "outside" side of the network and handles 'in'coming traffic. + def rules_ACL_id + outside_interface_data = self['firewallInterfaces'].find { |interface_data| interface_data['name'] == 'outside' } + incoming_ACL = outside_interface_data['firewallContextAccessControlLists'].find { |firewallACL_data| firewallACL_data['direction'] == 'in' } if outside_interface_data + + if incoming_ACL + return incoming_ACL['id'] + else + return nil + end + end + + def self.vlan_firewall_mask + return "mask[primaryRouter,highAvailabilityFirewallFlag," + + "firewallInterfaces.firewallContextAccessControlLists," + + "networkVlanFirewall[id,datacenter,primaryIpAddress,firewallType,fullyQualifiedDomainName,billingItem.id]]" + end + + def self.default_rules_mask + return { "mask" => default_rules_mask_keys }.to_sl_object_mask + end + + def self.default_rules_mask_keys + ['orderValue', + 'action', + 'destinationIpAddress', + 'destinationIpSubnetMask', + 'protocol', + 'destinationPortRangeStart', + 'destinationPortRangeEnd', + 'sourceIpAddress', + 'sourceIpSubnetMask', + 'version'] + end + end # class Firewall +end # module SoftLayer \ No newline at end of file diff --git a/lib/softlayer/VLANFirewallOrder.rb b/lib/softlayer/VLANFirewallOrder.rb new file mode 100644 index 0000000..549e2bf --- /dev/null +++ b/lib/softlayer/VLANFirewallOrder.rb @@ -0,0 +1,93 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + # + # This class allows you to order a Firewall for a VLAN + # + class VLANFirewallOrder + ## + # The VLAN that you are ordering the firewall for. + attr_reader :vlan_id + + ## + # Set high_availabilty to true if you want redundant + # firewall devices (defaults to false, no high_availability) + attr_accessor :high_availability + + ## + # Create a new order for the given VLAN + # Note that the vlan_id is NOT the same as the vlan number. + def initialize (vlan_id, client = nil) + @softlayer_client = client || Client.default_client + raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !@softlayer_client + + @vlan_id = vlan_id + @high_availability = false + end + + ## + # Calls the SoftLayer API to verify that the template provided by this order is valid + # This routine will return the order template generated by the API or will throw an exception + # + # This routine will not actually create a Bare Metal Instance and will not affect billing. + # + # If you provide a block, it will receive the order template as a parameter and + # the block may make changes to the template before it is submitted. + def verify() + order_template = firewall_order_template + order_template = yield order_template if block_given? + + @softlayer_client[:Product_Order].verifyOrder(order_template) + end + + ## + # Calls the SoftLayer API to place an order for a new server based on the template in this + # order. If this succeeds then you will be billed for the new server. + # + # If you provide a block, it will receive the order template as a parameter and + # the block may make changes to the template before it is submitted. + def place_order!() + order_template = firewall_order_template + order_template = yield order_template if block_given? + + @softlayer_client[:Product_Order].placeOrder(order_template) + end + + protected + + ## + # Returns a hash of the creation options formatted to be sent to + # the SoftLayer API for either verification or completion + def firewall_order_template + client = @softlayer_client + additional_products_package = SoftLayer::ProductPackage.additional_products_package(client) + + template = { + 'complexType' => 'SoftLayer_Container_Product_Order_Network_Protection_Firewall_Dedicated', + 'quantity' => 1, + 'packageId' => additional_products_package.id, + 'vlanId' => @vlan_id + } + + if @high_availability + expected_description = "Hardware Firewall (High Availability)" + else + expected_description = "Hardware Firewall (Dedicated)" + end + + firewall_items = additional_products_package.items_with_description(expected_description) + + raise "Could not find a price item matching the description '#{expected_description}'" if firewall_items.empty? + + firewall_item = firewall_items[0] + + template['prices'] = [{ 'id' => firewall_item.price_id }] if firewall_item.respond_to?(:price_id) + + template + end + end # class VLANFirewallOrder +end # module SoftLayer diff --git a/lib/softlayer/VirtualServer.rb b/lib/softlayer/VirtualServer.rb index 31f5820..5116997 100644 --- a/lib/softlayer/VirtualServer.rb +++ b/lib/softlayer/VirtualServer.rb @@ -1,26 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# - -require 'time' +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer ## @@ -90,49 +72,6 @@ def cancel! self.service.deleteObject() end - ## - # This routine submits an order to upgrade the cpu count of the virtual server. - # The order may result in additional charges being applied to SoftLayer account - # - # This routine can also "downgrade" servers (set their cpu count lower) - # - # The routine returns true if the order is placed and false if it is not - # - def upgrade_cores!(num_cores) - upgrade_item_price = _item_price_in_category("guest_core", num_cores) - _order_upgrade_item!(upgrade_item_price) if upgrade_item_price - nil != upgrade_item_price - end - - ## - # This routine submits an order to change the RAM available to the virtual server. - # Pass in the desired amount of RAM for the server in Gigabytes - # - # The order may result in additional charges being applied to SoftLayer account - # - # The routine returns true if the order is placed and false if it is not - # - def upgrade_RAM!(ram_in_GB) - upgrade_item_price = _item_price_in_category("ram", ram_in_GB) - _order_upgrade_item!(upgrade_item_price) if upgrade_item_price - nil != upgrade_item_price - end - - ## - # This routine submits an order to change the maximum nic speed of the server - # Pass in the desired speed in Megabits per second (typically 10, 100, or 1000) - # (since you may choose a slower speed this routine can also be used for "downgrades") - # - # The order may result in additional charges being applied to SoftLayer account - # - # The routine returns true if the order is placed and false if it is not - # - def upgrade_max_port_speed!(network_speed_in_Mbps) - upgrade_item_price = _item_price_in_category("port_speed", network_speed_in_Mbps) - _order_upgrade_item!(upgrade_item_price) if upgrade_item_price - nil != upgrade_item_price - end - ## # Capture a disk image of this virtual server for use with other servers. # @@ -143,8 +82,14 @@ def upgrade_max_port_speed!(network_speed_in_Mbps) # # The image_notes should be a string and will be added to the image as notes. # + # The routine returns the instance of SoftLayer::ImageTemplate that is + # created. That image template will probably not be available immediately, however. + # You may use the wait_until_ready routine of SoftLayer::ImageTemplate to + # wait on it. + # def capture_image(image_name, include_attached_storage = false, image_notes = '') image_notes = '' if !image_notes + image_name = 'Captured Image' if !image_name disk_filter = lambda { |disk| disk['device'] == '0' } disk_filter = lambda { |disk| disk['device'] != '1' } if include_attached_storage @@ -152,6 +97,9 @@ def capture_image(image_name, include_attached_storage = false, image_notes = '' disks = self.blockDevices.select(&disk_filter) self.service.createArchiveTransaction(image_name, disks, image_notes) if disks && !disks.empty? + + image_templates = SoftLayer::ImageTemplate.find_private_templates(:name => image_name) + image_templates[0] if !image_templates.empty? end ## @@ -216,7 +164,7 @@ def self.server_with_id(server_id, options = {}) softlayer_client = options[:client] || Client.default_client raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - vg_service = softlayer_client["Virtual_Guest"] + vg_service = softlayer_client[:Virtual_Guest] vg_service = vg_service.object_mask(default_object_mask.to_sl_object_mask) if options.has_key?(:object_mask) @@ -260,10 +208,15 @@ def self.find_servers(options_hash = {}) softlayer_client = options_hash[:client] || Client.default_client raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client - object_filter = {} + if(options_hash.has_key? :object_filter) + object_filter = options_hash[:object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless object_filter.kind_of?(SoftLayer::ObjectFilter) + else + object_filter = ObjectFilter.new() + end option_to_filter_path = { - :cpus => "virtualGuests.maxCpu", + :cores => "virtualGuests.maxCpu", :memory => "virtualGuests.maxMemory", :hostname => "virtualGuests.hostname", :domain => "virtualGuests.domain", @@ -282,23 +235,23 @@ def self.find_servers(options_hash = {}) # that particular option, add a clause to the object filter that filters for the matching # value option_to_filter_path.each do |option, filter_path| - object_filter.merge!(SoftLayer::ObjectFilter.build(filter_path, options_hash[option])) if options_hash.has_key?(option) + object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option])} if options_hash[option] end # Tags get a much more complex object filter operation so we handle them separately if options_hash.has_key?(:tags) - object_filter.merge!(SoftLayer::ObjectFilter.build("virtualGuests.tagReferences.tag.name", { + object_filter.set_criteria_for_key_path("virtualGuests.tagReferences.tag.name", { 'operation' => 'in', 'options' => [{ 'name' => 'data', - 'value' => options_hash[:tags] + 'value' => options_hash[:tags].collect{ |tag_value| tag_value.to_s } }] - } )); + } ); end required_properties_mask = 'mask.id' - account_service = softlayer_client['Account'] + account_service = softlayer_client[:Account] account_service = account_service.object_filter(object_filter) unless object_filter.empty? account_service = account_service.object_mask(default_object_mask.to_sl_object_mask) @@ -357,34 +310,7 @@ def self.default_object_mask # For VirtualServers the service is +SoftLayer_Virtual_Guest+ and # addressing this object is done by id. def service - return softlayer_client["Virtual_Guest"].object_with_id(self.id) - end - - private - - ## - # Searches through the upgrade items pricess known to this server for the one that is in a particular category - # and whose capacity matches the value given. Returns the item_price or nil - # - def _item_price_in_category(which_category, capacity) - item_prices_in_category = self.upgrade_options.select { |item_price| item_price["categories"].find { |category| category["categoryCode"] == which_category } } - item_prices_in_category.find { |ram_item| ram_item["item"]["capacity"].to_i == capacity} - end - - ## - # Constructs an upgrade order to order the given item price. - # The order is built to execute immediately - # - def _order_upgrade_item!(upgrade_item_price) - # put together an order - upgrade_order = { - 'complexType' => 'SoftLayer_Container_Product_Order_Virtual_Guest_Upgrade', - 'virtualGuests' => [{'id' => self.id }], - 'properties' => [{'name' => 'MAINTENANCE_WINDOW', 'value' => Time.now.iso8601}], - 'prices' => [ upgrade_item_price ] - } - - self.softlayer_client["Product_Order"].placeOrder(upgrade_order) + return softlayer_client[:Virtual_Guest].object_with_id(self.id) end end #class VirtualServer end \ No newline at end of file diff --git a/lib/softlayer/VirtualServerOrder.rb b/lib/softlayer/VirtualServerOrder.rb index 4675484..7463a63 100644 --- a/lib/softlayer/VirtualServerOrder.rb +++ b/lib/softlayer/VirtualServerOrder.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer # @@ -38,9 +22,7 @@ class VirtualServerOrder # a virtual server #++ - # String, short name of the data center that will house the new virtual server (e.g. "dal05" or "sea01") - # Corresponds to +datacenter.name+ in the documentation for createObject. If not provided, the server will - # be provisioned in the first available data center. + # An instance of SoftLayer::Datacenter. The server will be provisioned in that Datacenter. attr_accessor :datacenter # String, The hostname to assign to the new server @@ -58,17 +40,18 @@ class VirtualServerOrder attr_accessor :memory #-- - # These two options are mutually exclusive, but one or the other must be provided. - # If you provide both, the image_global_id will be added to the order and the os_reference_code will be ignored + # These two options are mutually exclusive, but one of them must be provided. + # If you provide both, the image_template will be added to the order and the + # os_reference_code will be ignored #++ # String, An OS reference code for the operating system to install on the virtual server # Corresponds to +operatingSystemReferenceCode+ in the +createObject+ documentation attr_accessor :os_reference_code - # String, The globalIdentifier of a disk image to put on the newly created server - # Corresponds to +blockDeviceTemplateGroup.globalIdentifier+ in the +createObject+ documentation - attr_accessor :image_global_id + # An instance of the SoftLayer::ImageTemplate class. Represents the image template that should + # be installed on the server. + attr_accessor :image_template #-- # Optional attributes @@ -136,7 +119,7 @@ def verify() order_template = virtual_guest_template order_template = yield order_template if block_given? - @softlayer_client["Virtual_Guest"].generateOrderTemplate(order_template) + @softlayer_client[:Virtual_Guest].generateOrderTemplate(order_template) end # Calls the SoftLayer API to place an order for a new virtual server based on the template in this @@ -149,8 +132,8 @@ def place_order!() order_template = virtual_guest_template order_template = yield order_template if block_given? - virtual_server_hash = @softlayer_client["Virtual_Guest"].createObject(order_template) - SoftLayer::VirtualServer.server_with_id(virtual_server_hash["id"], :client => @softlayer_client) if virtual_server_hash + virtual_server_hash = @softlayer_client[:Virtual_Guest].createObject(order_template) + SoftLayer::VirtualServer.server_with_id(virtual_server_hash['id'], :client => @softlayer_client) if virtual_server_hash end protected @@ -170,21 +153,21 @@ def virtual_guest_template "hourlyBillingFlag" => !!@hourly } - template["dedicatedAccountHostOnlyFlag"] = true if @dedicated_host_only - template["privateNetworkOnlyFlag"] = true if @private_network_only + template['dedicatedAccountHostOnlyFlag'] = true if @dedicated_host_only + template['privateNetworkOnlyFlag'] = true if @private_network_only - template["datacenter"] = {"name" => @datacenter} if @datacenter + template['datacenter'] = {"name" => @datacenter.name} if @datacenter template['userData'] = [{'value' => @user_metadata}] if @user_metadata template['networkComponents'] = [{'maxSpeed'=> @max_port_speed}] if @max_port_speed template['postInstallScriptUri'] = @provision_script_URI.to_s if @provision_script_URI template['sshKeys'] = @ssh_key_ids.collect { |ssh_key_id| {'id'=> ssh_key_id.to_i } } if @ssh_key_ids template['primaryNetworkComponent'] = { "networkVlan" => { "id" => @public_vlan_id.to_i } } if @public_vlan_id - template["primaryBackendNetworkComponent"] = { "networkVlan" => {"id" => @private_vlan_id.to_i } } if @private_vlan_id + template['primaryBackendNetworkComponent'] = { "networkVlan" => {"id" => @private_vlan_id.to_i } } if @private_vlan_id - if @image_global_id - template["blockDeviceTemplateGroup"] = {"globalIdentifier" => @image_global_id} + if @image_template + template['blockDeviceTemplateGroup'] = {"globalIdentifier" => @image_template.global_id} elsif @os_reference_code - template["operatingSystemReferenceCode"] = @os_reference_code + template['operatingSystemReferenceCode'] = @os_reference_code end if @disks && !@disks.empty? @@ -213,7 +196,7 @@ def self.create_object_options(client = nil) raise "#{__method__} requires a client but none was given and Client::default_client is not set" if !softlayer_client @@create_object_options ||= nil - @@create_object_options = softlayer_client["Virtual_Guest"].getCreateObjectOptions() if !@@create_object_options + @@create_object_options = softlayer_client[:Virtual_Guest].getCreateObjectOptions() if !@@create_object_options @@create_object_options end @@ -227,37 +210,37 @@ def self.create_object_options(client = nil) ## # Return a list of values that are valid for the :datacenter attribute def self.datacenter_options(client = nil) - create_object_options(client)["datacenters"].collect { |datacenter_spec| datacenter_spec['template']['datacenter']["name"] }.uniq.sort! + create_object_options(client)['datacenters'].collect { |datacenter_spec| Datacenter.datacenter_named(datacenter_spec['template']['datacenter']['name'], client) }.uniq end ## # Return a list of values that are valid for the :cores attribute def self.core_options(client = nil) - create_object_options(client)["processors"].collect { |processor_spec| processor_spec['template']['startCpus'] }.uniq.sort! + create_object_options(client)['processors'].collect { |processor_spec| processor_spec['template']['startCpus'] }.uniq.sort! end ## # Return a list of values that are valid for the :memory attribute def self.memory_options(client = nil) - create_object_options(client)["memory"].collect { |memory_spec| memory_spec['template']['maxMemory'].to_i / 1024}.uniq.sort! + create_object_options(client)['memory'].collect { |memory_spec| memory_spec['template']['maxMemory'].to_i / 1024}.uniq.sort! end ## # Return a list of values that are valid the array given to the :disks def self.disk_options(client = nil) - create_object_options(client)["blockDevices"].collect { |block_device_spec| block_device_spec['template']['blockDevices'][0]['diskImage']['capacity']}.uniq.sort! + create_object_options(client)['blockDevices'].collect { |block_device_spec| block_device_spec['template']['blockDevices'][0]['diskImage']['capacity']}.uniq.sort! end ## # Returns a list of the valid :os_refrence_codes def self.os_reference_code_options(client = nil) - create_object_options(client)["operatingSystems"].collect { |os_spec| os_spec['template']['operatingSystemReferenceCode'] }.uniq.sort! + create_object_options(client)['operatingSystems'].collect { |os_spec| os_spec['template']['operatingSystemReferenceCode'] }.uniq.sort! end ## # Returns a list of the :max_port_speeds def self.max_port_speed_options(client = nil) - create_object_options(client)["networkComponents"].collect { |component_spec| component_spec['template']['networkComponents'][0]['maxSpeed'] } + create_object_options(client)['networkComponents'].collect { |component_spec| component_spec['template']['networkComponents'][0]['maxSpeed'] } end end # class VirtualServerOrder end # module SoftLayer diff --git a/lib/softlayer/VirtualServerUpgradeOrder.rb b/lib/softlayer/VirtualServerUpgradeOrder.rb new file mode 100644 index 0000000..9c56da2 --- /dev/null +++ b/lib/softlayer/VirtualServerUpgradeOrder.rb @@ -0,0 +1,142 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + # This class is used to order changes to a virtual server. Although + # the class is named "upgrade" this class can also be used for "downgrades" + # (i.e. changing attributes to a smaller, or slower, value) + # + # The class can also be used to discover what upgrades are available + # for a given virtual server. + # + class VirtualServerUpgradeOrder + # The virtual server that this order is designed to upgrade. + attr_reader :virtual_server + + # The number of cores the server should have after the upgrade. + # If this is nil, the the number of cores will not change + attr_accessor :cores + + # The amount of RAM (in GB) that the server should have after the upgrade + # If this is nil, the ram will not change + attr_accessor :ram + + # The port speed (in Mega bits per second) that the server should have + # after the upgrade. This is typically a value like 100, or 1000 + # If this is nil, the port speeds will not change + attr_accessor :max_port_speed + + # The date and time when you would like the upgrade to be processed. + # This should simply be a Time object. If nil then the upgrade + # will be performed immediately + attr_accessor :upgrade_at + + ## + # Create an upgrade order for the virtual server provided. + # + def initialize(virtual_server) + raise "A virtual server must be provided at the time a virtual server order is created" if !virtual_server || !virtual_server.kind_of?(SoftLayer::VirtualServer) + @virtual_server = virtual_server + end + + ## + # Sends the order represented by this object to SoftLayer for validation. + # + # If a block is passed to verify, the code will send the order template + # being constructed to the block before the order is actually sent for + # validation. + # + def verify() + if has_order_items? + order_object = self.order_object + order_object = yield order_object if block_given? + + @virtual_server.softlayer_client[:Product_Order].verifyOrder(order_object) + end + end + + ## + # Places the order represented by this object. This is likely to + # involve a change to the charges on an account. + # + # If a block is passed to this routine, the code will send the order template + # being constructed to that block before the order is sent + # + def place_order!() + if has_order_items? + order_object = self.order_object + order_object = yield order_object if block_given? + + @virtual_server.softlayer_client[:Product_Order].placeOrder(order_object) + end + end + + ## + # Return a list of values that are valid for the :cores attribute + def core_options() + self._item_prices_in_category("guest_core").map { |item_price| item_price['item']['capacity'].to_i}.sort.uniq + end + + ## + # Return a list of values that are valid for the :memory attribute + def memory_options() + self._item_prices_in_category("ram").map { |item_price| item_price['item']['capacity'].to_i}.sort.uniq + end + + ## + # Returns a list of valid values for max_port_speed + def max_port_speed_options(client = nil) + self._item_prices_in_category("port_speed").map { |item_price| item_price['item']['capacity'].to_i}.sort.uniq + end + + private + + ## + # Returns true if this order object has any upgrades specified + # + def has_order_items? + @cores != nil || @ram != nil || @max_port_speed != nil + end + + ## + # Returns a list of the update item prices, in the given category, for the server + # + def _item_prices_in_category(which_category) + @virtual_server.upgrade_options.select { |item_price| item_price['categories'].find { |category| category['categoryCode'] == which_category } } + end + + ## + # Searches through the upgrade items pricess known to this server for the one that is in a particular category + # and whose capacity matches the value given. Returns the item_price or nil + # + def _item_price_with_capacity(which_category, capacity) + self._item_prices_in_category(which_category).find { |item_price| item_price['item']['capacity'].to_i == capacity} + end + + ## + # construct an order object + # + def order_object + prices = [] + + cores_price_item = @cores ? _item_price_with_capacity("guest_core", @cores) : nil + ram_price_item = @ram ? _item_price_with_capacity("ram", @ram) : nil + max_port_speed_price_item = @max_port_speed ? _item_price_with_capacity("port_speed", @max_port_speed) : nil + + prices << { "id" => cores_price_item['id'] } if cores_price_item + prices << { "id" => ram_price_item['id'] } if ram_price_item + prices << { "id" => max_port_speed_price_item['id'] } if max_port_speed_price_item + + # put together an order + upgrade_order = { + 'complexType' => 'SoftLayer_Container_Product_Order_Virtual_Guest_Upgrade', + 'virtualGuests' => [{'id' => @virtual_server.id }], + 'properties' => [{'name' => 'MAINTENANCE_WINDOW', 'value' => @upgrade_at ? @upgrade_at.iso8601 : Time.now.iso8601}], + 'prices' => prices + } + end + end # VirtualServerUpgradeOrder +end # SoftLayer Module diff --git a/lib/softlayer/base.rb b/lib/softlayer/base.rb index 1151e52..6f012a7 100644 --- a/lib/softlayer/base.rb +++ b/lib/softlayer/base.rb @@ -1,37 +1,18 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ require 'rubygems' -# This module is used to provide a namespace for SoftLayer code. It also declares a number of -# global variables: -# - +$SL_API_USERNAME+ - The default username passed by clients to the server for authentication. -# Set this if you want to use the same username for all clients and don't want to have to specify it when the client is created -# - +$SL_API_KEY+ - The default API key passed by clients to the server for authentication. -# Set this if you want to use the same api for all clients and don't want to have to specify it when the client is created -# - +$SL_API_BASE_URL+- The default URL used to access the SoftLayer API. This defaults to the value of +SoftLayer::API_PUBLIC_ENDPOINT+ +## +# The SoftLayer module provides a namespace for SoftLayer code. # module SoftLayer - VERSION = "2.2.2" # version history in the CHANGELOG.textile file at the root of the source + # The version number (including major, minor, and bugfix numbers) + # This should change in accordance with the concept of Semantic Versioning + VERSION = "3.0.0" # version history in the CHANGELOG.textile file at the root of the source # The base URL of the SoftLayer API available to the public internet. API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3/' @@ -39,9 +20,9 @@ module SoftLayer # The base URL of the SoftLayer API available through SoftLayer's private network API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3/' - # + #-- # These globals can be used to simplify client creation - # + #++ # Set this if you want to provide a default username for each client as it is created. # usernames provided to the client initializer will override the global @@ -59,4 +40,4 @@ module SoftLayer # # History: # -# The history has been moved to the CHANGELOG.textile file in the source directory +# The history can be found in the CHANGELOG.textile file in the project root directory diff --git a/lib/softlayer/object_mask_helpers.rb b/lib/softlayer/object_mask_helpers.rb index a668ac8..7f70faf 100644 --- a/lib/softlayer/object_mask_helpers.rb +++ b/lib/softlayer/object_mask_helpers.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ ## # This extension to the Hash class to allows object masks to be constructed diff --git a/lib/softlayer_api.rb b/lib/softlayer_api.rb index 70b20ca..e362867 100755 --- a/lib/softlayer_api.rb +++ b/lib/softlayer_api.rb @@ -1,45 +1,40 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ +# requirements from the core libraries +require 'time' + +# Requirements for the Foundation Layer require 'softlayer/base' require 'softlayer/object_mask_helpers' require 'softlayer/APIParameterFilter' require 'softlayer/ObjectFilter' require 'softlayer/ObjectMaskParser' require 'softlayer/Config' - require 'softlayer/Client' require 'softlayer/Service' -# model classes +# Requirements for the Model Layer require 'softlayer/ModelBase' +require 'softlayer/Datacenter' require 'softlayer/DynamicAttribute' require 'softlayer/Account' -require 'softlayer/Ticket' require 'softlayer/Server' require 'softlayer/BareMetalServer' require 'softlayer/BareMetalServerOrder' require 'softlayer/BareMetalServerOrder_Package' +require 'softlayer/ImageTemplate' +require 'softlayer/NetworkComponent' require 'softlayer/ProductPackage' require 'softlayer/ProductItemCategory' +require 'softlayer/ServerFirewall' +require 'softlayer/ServerFirewallOrder' +require 'softlayer/Ticket' require 'softlayer/VirtualServer' require 'softlayer/VirtualServerOrder' +require 'softlayer/VirtualServerUpgradeOrder' +require 'softlayer/VLANFirewall' +require 'softlayer/VLANFirewallOrder' diff --git a/rakefile b/rakefile index 95f4d09..6a1a4ce 100644 --- a/rakefile +++ b/rakefile @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__))) diff --git a/softlayer_api.gemspec b/softlayer_api.gemspec index 00746e0..831fbe3 100644 --- a/softlayer_api.gemspec +++ b/softlayer_api.gemspec @@ -27,5 +27,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'rspec' s.add_development_dependency 'rdoc', '>=2.4.2' s.add_development_dependency 'json', '~> 1.8', '>= 1.8.1' - s.add_development_dependency 'coveralls' + s.add_development_dependency 'coveralls' end diff --git a/spec/APIParameterFilter_spec.rb b/spec/APIParameterFilter_spec.rb index 5cc1b37..493b2eb 100644 --- a/spec/APIParameterFilter_spec.rb +++ b/spec/APIParameterFilter_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -58,7 +42,7 @@ end it "rejects calls that pass things other than strings" do - expect { filter.object_mask(["anArray"]) }.to raise_error + expect { filter.object_mask(['anArray']) }.to raise_error expect { filter.object_mask({"a" => "hash"}) }.to raise_error expect { filter.object_mask(Object.new) }.to raise_error end @@ -94,7 +78,7 @@ it "stores its value in server_object_filter when called" do test_filter = SoftLayer::ObjectFilter.new() - test_filter["fish"] = "cow" + test_filter.set_criteria_for_key_path("fish", "cow") result = filter.object_filter(test_filter) expect(result.server_object_filter).to eq({"fish" => "cow"}) @@ -106,7 +90,7 @@ target = double("method_missing_target") filter = SoftLayer::APIParameterFilter.new(target).object_mask("mask.fish", "mask[cow]", "mask(typed).duck", "mask(typed)[chicken]").object_with_id(12345) - expect(target).to receive(:call_softlayer_api_with_params).with(:getObject, filter, ["marshmallow"]) + expect(target).to receive(:call_softlayer_api_with_params).with(:getObject, filter, ['marshmallow']) filter.getObject("marshmallow") end diff --git a/spec/Account_spec.rb b/spec/Account_spec.rb index 59e8402..118d11a 100644 --- a/spec/Account_spec.rb +++ b/spec/Account_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -42,7 +26,7 @@ it "identifies itself with the Account service" do mock_client = SoftLayer::Client.new(:username => "fake_user", :api_key => "BADKEY") allow(mock_client).to receive(:[]) do |service_name| - expect(service_name).to eq "Account" + expect(service_name).to eq :Account mock_service = SoftLayer::Service.new("SoftLayer_Account", :client => mock_client) # mock out call_softlayer_api_with_params so the service doesn't actually try to @@ -60,7 +44,7 @@ it "should allow the user to get the default account for a service" do test_client = double("mockClient") allow(test_client).to receive(:[]) do |service_name| - expect(service_name).to eq "Account" + expect(service_name).to eq :Account test_service = double("mockService") allow(test_service).to receive(:getObject) do @@ -96,10 +80,25 @@ end end + it "fetches a list of open tickets" do + mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "fake_api_key") + account_service = mock_client[:Account] + + expect(account_service).to receive(:call_softlayer_api_with_params).with(:getOpenTickets, instance_of(SoftLayer::APIParameterFilter),[]) do + fixture_from_json("test_tickets") + end + + test_account = SoftLayer::Account.new(mock_client, fixture_from_json("test_account")) + open_tickets = nil + expect { open_tickets = test_account.open_tickets }.to_not raise_error + ticket_ids = open_tickets.collect { |ticket| ticket.id } + expect(ticket_ids.sort).to eq [12345, 12346, 12347, 12348, 12349].sort + end + describe "relationship to servers" do it "should respond to a request for servers" do mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "fake_api_key") - account_service = mock_client["Account"] + account_service = mock_client[:Account] allow(account_service).to receive(:getObject).and_return(fixture_from_json("test_account")) allow(account_service).to receive(:call_softlayer_api_with_params) do |api_method, api_filter, arguments| case api_method @@ -109,7 +108,7 @@ fixture_from_json("test_virtual_servers") when :getObject fixture_from_json("test_account") - end + end end test_account = SoftLayer::Account.account_for_client(mock_client) diff --git a/spec/BareMetalServerOrder_Package_spec.rb b/spec/BareMetalServerOrder_Package_spec.rb index 7dd7b7f..290b22e 100644 --- a/spec/BareMetalServerOrder_Package_spec.rb +++ b/spec/BareMetalServerOrder_Package_spec.rb @@ -39,14 +39,19 @@ SoftLayer::BareMetalServerOrder_Package.new(package, client) end + let (:test_datacenter) do + client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') + SoftLayer::Datacenter.new(client,'id' => 224092, 'name' => 'sng01') + end + it 'places the package id from which it was ordered into the order template' do - expect(test_order.hardware_order["packageId"]).to eq 32 + expect(test_order.hardware_order['packageId']).to eq 32 end it "places its :datacenter attribute into the order template" do - expect(test_order.hardware_order["location"]).to be_nil - test_order.datacenter = "sng01" - expect(test_order.hardware_order["location"]).to eq 224092 + expect(test_order.hardware_order['location']).to be_nil + test_order.datacenter = test_datacenter + expect(test_order.hardware_order['location']).to eq 224092 end it "places its :hostname attribute into the hardware template in the order" do @@ -104,11 +109,11 @@ def config_option_2.price_id client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') package = SoftLayer::ProductPackage.new(client, fixture_from_json("Product_Package")) - order_service = client["Product_Order"] + order_service = client[:Product_Order] allow(order_service).to receive(:call_softlayer_api_with_params) test_order = SoftLayer::BareMetalServerOrder_Package.new(package, client) - test_order.datacenter = 'sng01' + test_order.datacenter = test_datacenter test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" test_order.configuration_options = { 'category' => 123 } @@ -121,11 +126,11 @@ def config_option_2.price_id client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') package = SoftLayer::ProductPackage.new(client, fixture_from_json("Product_Package")) - order_service = client["Product_Order"] + order_service = client[:Product_Order] allow(order_service).to receive(:call_softlayer_api_with_params) test_order = SoftLayer::BareMetalServerOrder_Package.new(package, client) - test_order.datacenter = 'sng01' + test_order.datacenter = test_datacenter test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" test_order.configuration_options = { 'category' => 123 } @@ -138,11 +143,11 @@ def config_option_2.price_id client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') package = SoftLayer::ProductPackage.new(client, fixture_from_json("Product_Package")) - order_service = client["Product_Order"] + order_service = client[:Product_Order] allow(order_service).to receive(:call_softlayer_api_with_params) test_order = SoftLayer::BareMetalServerOrder_Package.new(package, client) - test_order.datacenter = 'sng01' + test_order.datacenter = test_datacenter test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" test_order.configuration_options = { 'category' => 123 } @@ -156,11 +161,11 @@ def config_option_2.price_id client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') package = SoftLayer::ProductPackage.new(client, fixture_from_json("Product_Package")) - order_service = client["Product_Order"] + order_service = client[:Product_Order] allow(order_service).to receive(:call_softlayer_api_with_params) test_order = SoftLayer::BareMetalServerOrder_Package.new(package, client) - test_order.datacenter = 'sng01' + test_order.datacenter = test_datacenter test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" test_order.configuration_options = { 'category' => 123 } diff --git a/spec/BareMetalServerOrder_spec.rb b/spec/BareMetalServerOrder_spec.rb index e6c4f9b..a5c5b66 100644 --- a/spec/BareMetalServerOrder_spec.rb +++ b/spec/BareMetalServerOrder_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -50,68 +34,70 @@ end it "places its :datacenter attribute into the order template" do - expect(subject.hardware_instance_template["datacenter"]).to be_nil - subject.datacenter = "dal05" - expect(subject.hardware_instance_template["datacenter"]).to eq({ "name" => "dal05" }) + client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') + + expect(subject.hardware_instance_template['datacenter']).to be_nil + subject.datacenter = SoftLayer::Datacenter.new(client, 'id' => 42, 'name' => "dal05") + expect(subject.hardware_instance_template['datacenter']).to eq({ "name" => "dal05" }) end it "places its :hostname attribute into the order template" do - expect(subject.hardware_instance_template["hostname"]).to be_nil + expect(subject.hardware_instance_template['hostname']).to be_nil subject.hostname = "testhostname" - expect(subject.hardware_instance_template["hostname"]).to eq "testhostname" + expect(subject.hardware_instance_template['hostname']).to eq "testhostname" end it "places its :domain attribute into the order template" do - expect(subject.hardware_instance_template["domain"]).to be_nil + expect(subject.hardware_instance_template['domain']).to be_nil subject.domain = "softlayer.com" - expect(subject.hardware_instance_template["domain"]).to eq "softlayer.com" + expect(subject.hardware_instance_template['domain']).to eq "softlayer.com" end it "places its :cores attribute into the order template as startCpus" do subject.cores = 4 - expect(subject.hardware_instance_template["processorCoreAmount"]).to eq 4 + expect(subject.hardware_instance_template['processorCoreAmount']).to eq 4 end it "places the :memory attrbute in the template as memoryCapacity" do subject.memory = 4 - expect(subject.hardware_instance_template["memoryCapacity"]).to eq 4 + expect(subject.hardware_instance_template['memoryCapacity']).to eq 4 end it "places an OS identifier into the order template as the operatingSystemReferenceCode" do - expect(subject.hardware_instance_template["operatingSystemReferenceCode"]).to be_nil + expect(subject.hardware_instance_template['operatingSystemReferenceCode']).to be_nil subject.os_reference_code = 'UBUNTU_12_64' expect(subject.hardware_instance_template['operatingSystemReferenceCode']).to eq 'UBUNTU_12_64' end it "places the attribute :hourly into the template as hourlyBillingFlag converting the value to a boolean constant" do # note, we don't want the flag to be nil we want it to be eotjer false or true - expect(subject.hardware_instance_template["hourlyBillingFlag"]).to be(false) + expect(subject.hardware_instance_template['hourlyBillingFlag']).to be(false) subject.hourly = true - expect(subject.hardware_instance_template["hourlyBillingFlag"]).to be(true) + expect(subject.hardware_instance_template['hourlyBillingFlag']).to be(true) subject.hourly = false - expect(subject.hardware_instance_template["hourlyBillingFlag"]).to be(false) + expect(subject.hardware_instance_template['hourlyBillingFlag']).to be(false) end it "puts the public VLAN id into an order template as primaryNetworkComponent.networkVlan.id" do - expect(subject.hardware_instance_template["primaryNetworkComponent"]).to be_nil + expect(subject.hardware_instance_template['primaryNetworkComponent']).to be_nil subject.public_vlan_id = 12345 - expect(subject.hardware_instance_template["primaryNetworkComponent"]).to eq({ "networkVlan" => { "id" => 12345 } }) + expect(subject.hardware_instance_template['primaryNetworkComponent']).to eq({ "networkVlan" => { "id" => 12345 } }) end it "puts the private VLAN id into an order template as primaryBackendNetworkComponent.networkVlan.id" do - expect(subject.hardware_instance_template["primaryBackendNetworkComponent"]).to be_nil + expect(subject.hardware_instance_template['primaryBackendNetworkComponent']).to be_nil subject.private_vlan_id = 12345 - expect(subject.hardware_instance_template["primaryBackendNetworkComponent"]).to eq({ "networkVlan" => { "id" => 12345 } }) + expect(subject.hardware_instance_template['primaryBackendNetworkComponent']).to eq({ "networkVlan" => { "id" => 12345 } }) end it "sets up disks in the order template as hardDrives" do - expect(subject.hardware_instance_template["hardDrives"]).to be_nil + expect(subject.hardware_instance_template['hardDrives']).to be_nil subject.disks = [2, 25, 50] # note that device id 1 should be skipped as SoftLayer reserves that id for OS swap space. - expect(subject.hardware_instance_template["hardDrives"]).to eq [ + expect(subject.hardware_instance_template['hardDrives']).to eq [ {"capacity"=>2}, {"capacity"=>25}, {"capacity"=>50} @@ -119,37 +105,37 @@ end it "puts the :ssh_key_ids in the template as sshKeys and breaks out the ids into objects" do - expect(subject.hardware_instance_template["sshKeys"]).to be_nil + expect(subject.hardware_instance_template['sshKeys']).to be_nil subject.ssh_key_ids = [123, 456, 789] expect(subject.hardware_instance_template['sshKeys']).to eq [{'id' => 123}, {'id' => 456}, {'id' => 789}] end it "puts the :provision_script_URI property into the template as postInstallScriptUri" do - expect(subject.hardware_instance_template["postInstallScriptUri"]).to be_nil + expect(subject.hardware_instance_template['postInstallScriptUri']).to be_nil subject.provision_script_URI = 'http:/provisionhome.mydomain.com/fancyscript.sh' expect(subject.hardware_instance_template['postInstallScriptUri']).to eq 'http:/provisionhome.mydomain.com/fancyscript.sh' end it "accepts URI objects for the provision script URI" do - expect(subject.hardware_instance_template["postInstallScriptUri"]).to be_nil + expect(subject.hardware_instance_template['postInstallScriptUri']).to be_nil subject.provision_script_URI = URI.parse('http:/provisionhome.mydomain.com/fancyscript.sh') expect(subject.hardware_instance_template['postInstallScriptUri']).to eq 'http:/provisionhome.mydomain.com/fancyscript.sh' end it "places the private_network_only attribute in the template as privateNetworkOnlyFlag" do - expect(subject.hardware_instance_template["privateNetworkOnlyFlag"]).to be_nil + expect(subject.hardware_instance_template['privateNetworkOnlyFlag']).to be_nil subject.private_network_only = true - expect(subject.hardware_instance_template["privateNetworkOnlyFlag"]).to be(true) + expect(subject.hardware_instance_template['privateNetworkOnlyFlag']).to be(true) end it "puts the user metadata string into the template as userData" do - expect(subject.hardware_instance_template["userData"]).to be_nil + expect(subject.hardware_instance_template['userData']).to be_nil subject.user_metadata = "MetadataValue" expect(subject.hardware_instance_template['userData']).to eq [{'value' => 'MetadataValue'}] end it "puts the max_port_speed attribute into the template as networkComponents.maxSpeed" do - expect(subject.hardware_instance_template["networkComponents"]).to be_nil + expect(subject.hardware_instance_template['networkComponents']).to be_nil subject.max_port_speed = 1000 expect(subject.hardware_instance_template['networkComponents']).to eq [{'maxSpeed' => 1000}] end @@ -163,7 +149,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - hardware_service = client["Hardware"] + hardware_service = client[:Hardware] allow(hardware_service).to receive(:call_softlayer_api_with_params) expect(hardware_service).to receive(:generateOrderTemplate).with(test_order.hardware_instance_template) @@ -179,7 +165,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - hardware_service = client["Hardware"] + hardware_service = client[:Hardware] allow(hardware_service).to receive(:call_softlayer_api_with_params) expect(hardware_service).to receive(:createObject).with(test_order.hardware_instance_template) @@ -195,7 +181,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - hardware_service = client["Hardware"] + hardware_service = client[:Hardware] allow(hardware_service).to receive(:call_softlayer_api_with_params) substituted_order_template = { 'aFake' => 'andBogusOrderTemplate' } @@ -212,7 +198,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - hardware_service = client["Hardware"] + hardware_service = client[:Hardware] allow(hardware_service).to receive(:call_softlayer_api_with_params) substituted_order_template = { 'aFake' => 'andBogusOrderTemplate' } @@ -223,13 +209,14 @@ describe "methods returning available options for attributes" do let (:client) do client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') - virtual_guest_service = client["Hardware"] + virtual_guest_service = client[:Hardware] allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) + fake_options = + allow(virtual_guest_service).to receive(:getCreateObjectOptions) { fixture_from_json("Hardware_createObjectOptions") } - fake_options = fixture_from_json("Hardware_createObjectOptions") - allow(virtual_guest_service).to receive(:getCreateObjectOptions) { - fake_options - } + location_service = client[:Location] + allow(location_service).to receive(:call_softlayer_api_with_params) + allow(location_service).to receive(:getDatacenters) {fixture_from_json("datacenter_locations")} client end @@ -239,7 +226,9 @@ end it "transmogrifies the datacenter options for the :datacenter attribute" do - expect(SoftLayer::BareMetalServerOrder.datacenter_options(client)).to eq ["ams01", "dal01", "dal05", "dal06", "sea01", "sjc01", "sng01", "wdc01"] + datacenter_options = SoftLayer::BareMetalServerOrder.datacenter_options(client) + datacenter_names = datacenter_options.map { |datacenter| datacenter.name }.sort + expect(datacenter_names).to eq ["ams01", "dal01", "dal05", "dal06", "sea01", "sjc01", "sng01", "wdc01"] end it "transmogrifies the processor create object options for the cores attribute" do diff --git a/spec/BareMetalServer_spec.rb b/spec/BareMetalServer_spec.rb index 14591a7..169c60f 100644 --- a/spec/BareMetalServer_spec.rb +++ b/spec/BareMetalServer_spec.rb @@ -1,24 +1,10 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ + + $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -40,20 +26,24 @@ SoftLayer::BareMetalServer.new(mock_client, { "id" => 12345 }) end - it "identifies itself with the SoftLayer_Hardware service" do + it "identifies itself with the SoftLayer_Hardware_Server service" do service = sample_server.service expect(service.server_object_id).to eq(12345) - expect(service.target.service_name).to eq "SoftLayer_Hardware" + expect(service.target.service_name).to eq "SoftLayer_Hardware_Server" end it_behaves_like "server with port speed" do let (:server) { sample_server } end + it_behaves_like "server with mutable hostname" do + let (:server) { sample_server } + end + it "can be cancelled" do mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "DEADBEEFBADF00D") allow(mock_client).to receive(:[]) do |service_name| - expect(service_name).to eq "Ticket" + expect(service_name).to eq :Ticket service = mock_client.service_named(service_name) expect(service).to receive(:createCancelServerTicket).with(12345, 'Migrating to larger server', 'moving on up!', true, 'HARDWARE') diff --git a/spec/Client_spec.rb b/spec/Client_spec.rb index 4046a3e..9181a52 100644 --- a/spec/Client_spec.rb +++ b/spec/Client_spec.rb @@ -104,7 +104,7 @@ client = SoftLayer::Client.new(:username => 'fake_user', :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/', :timeout => 60) expect(client.network_timeout).to eq 60 end - + it 'gets the default endpoint even if none is provided' do $SL_API_BASE_URL = nil client = SoftLayer::Client.new(:username => 'fake_user', :api_key => 'fake_key') @@ -162,32 +162,32 @@ end it "allows bracket dereferences as an alternate service syntax" do - test_service = test_client['Account'] + test_service = test_client[:Account] expect(test_service).to_not be_nil expect(test_service.service_name).to eq "SoftLayer_Account" expect(test_service.client).to be(test_client) end it "returns the same service repeatedly when asked more than once" do - first_account_service = test_client['Account'] + first_account_service = test_client[:Account] second_account_service = test_client.service_named('Account') expect(first_account_service).to be(second_account_service) end - + it "recognizes a symbol as an acceptable service name" do account_service = test_client[:Account] expect(account_service).to_not be_nil - - trying_again = test_client['Account'] + + trying_again = test_client[:Account] expect(trying_again).to be(account_service) - - yet_again = test_client['SoftLayer_Account'] + + yet_again = test_client[:SoftLayer_Account] expect(yet_again).to be(account_service) - + once_more = test_client[:SoftLayer_Account] expect(once_more).to be(account_service) end - + end end diff --git a/spec/Config_spec.rb b/spec/Config_spec.rb index 9618a49..668ab2d 100644 --- a/spec/Config_spec.rb +++ b/spec/Config_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) diff --git a/spec/Datacenter_spec.rb b/spec/Datacenter_spec.rb new file mode 100644 index 0000000..c09cd5d --- /dev/null +++ b/spec/Datacenter_spec.rb @@ -0,0 +1,50 @@ +# +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the 'Software'), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '../lib')) + +require 'rubygems' +require 'softlayer_api' +require 'rspec' + +describe SoftLayer::Datacenter do + let (:mock_client) do + mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "DEADBEEFBADF00D") + allow(mock_client[:Location]).to receive(:call_softlayer_api_with_params) do |method_name, parameters, args| + fixture_from_json('datacenter_locations.json') + end + + mock_client + end + + it "retrieves a list of datacenters" do + datacenters = SoftLayer::Datacenter.datacenters(mock_client) + names = datacenters.collect { |datacenter| datacenter.name } + expect(names.sort).to eq ["ams01", "dal01", "dal02", "dal04", "dal05", "dal06", "dal07", "hkg02", "hou02", "lon02", "sea01", "sjc01", "sng01", "tor01", "wdc01", "wdc03"] + end + + it "retrieves a particular datacenter by name" do + dal05 = SoftLayer::Datacenter.datacenter_named("dal05", mock_client) + expect(dal05.name).to eq "dal05" + expect(dal05.id).to be 138124 + end +end diff --git a/spec/DynamicAttribute_spec.rb b/spec/DynamicAttribute_spec.rb index f8cc135..738f3d2 100644 --- a/spec/DynamicAttribute_spec.rb +++ b/spec/DynamicAttribute_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) diff --git a/spec/ModelBase_spec.rb b/spec/ModelBase_spec.rb index 826f915..ede8988 100644 --- a/spec/ModelBase_spec.rb +++ b/spec/ModelBase_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -54,7 +38,7 @@ mock_client = double("Mock SoftLayer Client") test_model = SoftLayer::ModelBase.new(mock_client, { "id" => "12345"}); expect(test_model[:id]).to eq("12345") - expect(test_model["id"]).to eq("12345") + expect(test_model['id']).to eq("12345") end it "allows access to exposed softlayer properties" do diff --git a/spec/ObjectFilter_spec.rb b/spec/ObjectFilter_spec.rb index bf92b8b..1bdee80 100644 --- a/spec/ObjectFilter_spec.rb +++ b/spec/ObjectFilter_spec.rb @@ -27,115 +27,192 @@ require 'rspec' describe SoftLayer::ObjectFilter do - it "is empty hash when created" do - test_filter = SoftLayer::ObjectFilter.new() - expect(test_filter).to eq({}) + it "calls its construction block" do + block_called = false; + filter = SoftLayer::ObjectFilter.new() { + block_called = true; + } + + expect(block_called).to be(true) end - it "adds empty object filter sub-elements for unknown keys" do - test_filter = SoftLayer::ObjectFilter.new() - value = test_filter["foo"] + it "expects the methods in the ObjectFilterDefinitionContext to be available in its block" do + stuff_defined = false; + filter = SoftLayer::ObjectFilter.new() { + stuff_defined = !!defined?(satisfies_the_raw_condition); + } - expect(value).to_not be_nil - expect(value).to eq({}) - expect(value).to be_kind_of(SoftLayer::ObjectFilter) + expect(stuff_defined).to be(true) end - describe ":build" do - it "builds object filters from a key path and query string" do - object_filter = SoftLayer::ObjectFilter.build("hardware.domain", '*riak*'); - expect(object_filter).to eq({ - "hardware" => { - "domain" => { - 'operation' => '*= riak' - }}}) - end + it "is empty when no criteria have been added" do + filter = SoftLayer::ObjectFilter.new() + expect(filter.empty?).to be(true) + end - it "builds object filters from a key path and a hash" do - object_filter = SoftLayer::ObjectFilter.build("hardware.domain", {'bogus' => 'but fun'}); - expect(object_filter).to eq({ - "hardware" => { - "domain" => { - 'bogus' => 'but fun' - }}}) + it "is not empty criteria have been added" do + filter = SoftLayer::ObjectFilter.new do |filter| + filter.accept("foobar").when_it is("baz") end - it "builds object filters from a key path and am ObjectFilterOperation" do - filter_operation = SoftLayer::ObjectFilterOperation.new('~', 'wwdc') - object_filter = SoftLayer::ObjectFilter.build("hardware.domain", filter_operation); - expect(object_filter).to eq ({ - "hardware" => { - "domain" => { - 'operation' => '~ wwdc' - }}}) + expect(filter.empty?).to be(false) + end + + it "returns criteria for a given key path" do + test_hash = { 'one' => { 'two' => {'three' => 3}}} + + filter = SoftLayer::ObjectFilter.new() + filter.instance_eval do + @filter_hash = test_hash end - it "builds object filters from a key path and block" do - object_filter = SoftLayer::ObjectFilter.build("hardware.domain") { contains 'riak' }; - expect(object_filter).to eq ({ - "hardware" => { - "domain" => { - 'operation' => '*= riak' - } - } - }) + expect(filter.criteria_for_key_path("one")).to eq({'two' => {'three' => 3}}) + expect(filter.criteria_for_key_path("one.two")).to eq({'three' => 3}) + expect(filter.criteria_for_key_path("one.two.three")).to eq(3) + end + + it "returns nil when asked for criteria that don't exist" do + filter = SoftLayer::ObjectFilter.new() + filter.set_criteria_for_key_path("some.key.path", 3) + + expect(filter.criteria_for_key_path("some.key.path")).to eq(3) + expect(filter.criteria_for_key_path("does.not.exist")).to be_nil + + expect(filter.to_h).to eq({ 'some' => { 'key' => {'path' => 3}}}) + end + + it "changes criteria for a given key path" do + filter = SoftLayer::ObjectFilter.new() + filter.set_criteria_for_key_path("one.two.three", 3) + + expect(filter.criteria_for_key_path("one")).to eq({'two' => {'three' => 3}}) + expect(filter.criteria_for_key_path("one.two")).to eq({'three' => 3}) + expect(filter.criteria_for_key_path("one.two.three")).to eq(3) + + filter.set_criteria_for_key_path("one.two.also_two", 2) + expect(filter.criteria_for_key_path("one.two")).to eq({'also_two' => 2, 'three' => 3}) + expect(filter.criteria_for_key_path("one.two.also_two")).to eq(2) + + expect(filter.to_h).to eq({"one"=>{"two"=>{"three"=>3, "also_two"=>2}}}) + end + + it "sets criteria in the initializer with the fancy syntax" do + filter = SoftLayer::ObjectFilter.new do |filter| + filter.accept("some.key.path").when_it is(3) end + + expect(filter.criteria_for_key_path("some.key.path")).to eq({'operation' => 3}) + expect(filter.to_h).to eq({"some"=>{"key"=>{"path"=>{"operation"=>3}}}}) end - describe ":query_to_filter_operation" do - it "translates sample strings into valid operation structures" do - expect(SoftLayer::ObjectFilter.query_to_filter_operation('3')).to eq({'operation' => 3 }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('value')).to eq({'operation' => "_= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('value*')).to eq({'operation' => "^= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('*value')).to eq({'operation' => "$= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('*value*')).to eq({'operation' => "*= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('~ value')).to eq({'operation' => "~ value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('> value')).to eq({'operation' => "> value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('< value')).to eq({'operation' => "< value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('>= value')).to eq({'operation' => ">= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('<= value')).to eq({'operation' => "<= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('*= value')).to eq({'operation' => "*= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('^= value')).to eq({'operation' => "^= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('$= value')).to eq({'operation' => "$= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('_= value')).to eq({'operation' => "_= value" }) - expect(SoftLayer::ObjectFilter.query_to_filter_operation('!~ value')).to eq({'operation' => "!~ value" }) + it "allows the fancy syntax in a modify block" do + filter = SoftLayer::ObjectFilter.new() + + expect(filter.criteria_for_key_path("some.key.path")).to be_nil + + filter.modify do |filter| + filter.accept("some.key.path").when_it is(3) end + + expect(filter.criteria_for_key_path("some.key.path")).to eq({'operation' => 3}) + + # can replace a criterion + filter.modify do |filter| + filter.accept("some.key.path").when_it is(4) + end + + expect(filter.criteria_for_key_path("some.key.path")).to eq({'operation' => 4}) + end +end + +describe SoftLayer::ObjectFilterDefinitionContext do + it "defines the is matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is(" fred")).to eq({ 'operation' => ' fred' }) + expect(SoftLayer::ObjectFilterDefinitionContext.is(42)).to eq({ 'operation' => 42 }) end - describe ":build operations translate to correct operators" do - it "handles the common operators" do - object_filter = SoftLayer::ObjectFilter.build("domain") { contains 'value ' } - expect(object_filter).to eq({ "domain" => { 'operation' => "*= value"} }) + it "defines the is_not matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_not(" fred ")).to eq({ 'operation' => '!= fred' }) + end + + it "defines the contains matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.contains(" fred ")).to eq({ 'operation' => '*= fred' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { begins_with ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "^= value"} }) + it "defines the begins_with matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.begins_with(" fred ")).to eq({ 'operation' => '^= fred' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { ends_with ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "$= value"} }) + it "defines the ends_with matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.ends_with(" fred ")).to eq({ 'operation' => '$= fred' }) + end + + it "defines the matches_ignoring_case matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.matches_ignoring_case(" fred ")).to eq({ 'operation' => '_= fred' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { is 'value ' } - expect(object_filter).to eq({ "domain" => { 'operation' => "_= value"} }) + it "defines the is_greater_than matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_greater_than(" fred ")).to eq({ 'operation' => '> fred' }) + expect(SoftLayer::ObjectFilterDefinitionContext.is_greater_than(100)).to eq({ 'operation' => '> 100' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { is_not ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "!= value"} }) + it "defines the is_less_than matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_less_than(" fred ")).to eq({ 'operation' => '< fred' }) + expect(SoftLayer::ObjectFilterDefinitionContext.is_less_than(100)).to eq({ 'operation' => '< 100' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { is_greater_than 'value ' } - expect(object_filter).to eq({ "domain" => { 'operation' => "> value"} }) + it "defines the is_greater_or_equal_to matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_greater_or_equal_to(" fred ")).to eq({ 'operation' => '>= fred' }) + expect(SoftLayer::ObjectFilterDefinitionContext.is_greater_or_equal_to(100)).to eq({ 'operation' => '>= 100' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { is_less_than ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "< value"} }) + it "defines the is_less_or_equal_to matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_less_or_equal_to(" fred ")).to eq({ 'operation' => '<= fred' }) + expect(SoftLayer::ObjectFilterDefinitionContext.is_less_or_equal_to(100)).to eq({ 'operation' => '<= 100' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { is_greater_or_equal_to ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => ">= value"} }) + it "defines the contains_exactly matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.contains_exactly(" fred ")).to eq({ 'operation' => '~ fred' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { is_less_or_equal_to 'value ' } - expect(object_filter).to eq({ "domain" => { 'operation' => "<= value"} }) + it "defines the does_not_contain matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.does_not_contain(" fred ")).to eq({ 'operation' => '!~ fred' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { contains_exactly ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "~ value"} }) + it "defines the is_null matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_null()).to eq({ 'operation' => 'is null' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { does_not_contain ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "!~ value"} }) + it "defines the is_not_null matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_not_null()).to eq({ 'operation' => 'not null' }) + end + + it "defines the satisfies_the_raw_condition matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.satisfies_the_raw_condition( + { 'operation' => 'some_complex_operation_goes_here'})).to eq({ 'operation' => 'some_complex_operation_goes_here'}) + end + + it "allows 'matches_query' strings with operators" do + SoftLayer::OBJECT_FILTER_OPERATORS.each do |operator| + fake_string = "#{operator} fred " + expect(SoftLayer::ObjectFilterDefinitionContext.matches_query(fake_string)).to eq({ 'operation' => "#{operator} fred"}) end end -end + + it "allows 'matches_query' strings for exact value match" do + criteria = + expect(SoftLayer::ObjectFilterDefinitionContext.matches_query(" fred")).to eq({ 'operation' => "_= fred"}) + end + + it "allows 'matches_query' strings for begins_with" do + expect(SoftLayer::ObjectFilterDefinitionContext.matches_query("fred*")).to eq({ 'operation' => "^= fred"}) + end + + it "allows 'matches_query' strings for ends_with" do + expect(SoftLayer::ObjectFilterDefinitionContext.matches_query("*fred")).to eq({ 'operation' => "$= fred"}) + end + + it "allows 'matches_query' strings for contains" do + expect(SoftLayer::ObjectFilterDefinitionContext.matches_query("*fred*")).to eq({ 'operation' => "*= fred"}) + end +end \ No newline at end of file diff --git a/spec/ObjectMaskParser_spec.rb b/spec/ObjectMaskParser_spec.rb index 0de8026..9ebdc62 100644 --- a/spec/ObjectMaskParser_spec.rb +++ b/spec/ObjectMaskParser_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -61,7 +45,7 @@ expect(result.name).to eq 'filterMask' expect(result.children[0].name).to eq 'simple1' end - + it "should parse a mask set with fiterMask" do result = nil expect { result = subject.parse("[filterMask.simple1, filterMask.simple2]") }.to_not raise_error @@ -74,7 +58,7 @@ expect(result[1].name).to eq 'filterMask' expect(result[1].children.count).to eq 1 expect(result[1].children[0].name).to eq "simple2" - end + end end describe SoftLayer::ObjectMaskParser, "#parse_property_set" do diff --git a/spec/ObjectMaskProperty_spec.rb b/spec/ObjectMaskProperty_spec.rb index 090301f..44d09af 100644 --- a/spec/ObjectMaskProperty_spec.rb +++ b/spec/ObjectMaskProperty_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) diff --git a/spec/ProductPackage_spec.rb b/spec/ProductPackage_spec.rb index f3f01c9..43fbab4 100644 --- a/spec/ProductPackage_spec.rb +++ b/spec/ProductPackage_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -29,7 +13,7 @@ describe SoftLayer::ProductPackage do it "requests packages by key name" do client = SoftLayer::Client.new(:username => "fake_user", :api_key => "BADKEY") - product_package_service = client['Product_Package'] + product_package_service = client[:Product_Package] expect(product_package_service).to receive(:call_softlayer_api_with_params) do |method_name, parameters, args| expect(method_name).to be(:getAllObjects) @@ -39,13 +23,13 @@ [] end - SoftLayer::ProductPackage.packages_with_key_name('FAKE_KEY_NAME', client) + SoftLayer::ProductPackage.packages_with_key_name('FAKE_KEY_NAME', client) end it "identifies itself with the Product_Package service" do mock_client = SoftLayer::Client.new(:username => "fake_user", :api_key => "BADKEY") allow(mock_client).to receive(:[]) do |service_name| - expect(service_name).to eq "Product_Package" + expect(service_name).to eq :Product_Package mock_service = SoftLayer::Service.new("SoftLayer_Product_Package", :client => mock_client) # mock out call_softlayer_api_with_params so the service doesn't actually try to @@ -59,11 +43,11 @@ expect(fake_package.service.server_object_id).to eq(12345) expect(fake_package.service.target.service_name).to eq "SoftLayer_Product_Package" end - + describe "class methods for getting to packages" do let(:mock_client) do client = SoftLayer::Client.new(:username => "fake_user", :api_key => "BADKEY") - product_package_service = client['Product_Package'] + product_package_service = client[:Product_Package] allow(product_package_service).to receive(:call_softlayer_api_with_params).with(:getAllObjects, instance_of(SoftLayer::APIParameterFilter), []).and_return([fixture_from_json("Product_Package")]) client diff --git a/spec/ServerFirewall_spec.rb b/spec/ServerFirewall_spec.rb new file mode 100644 index 0000000..e293180 --- /dev/null +++ b/spec/ServerFirewall_spec.rb @@ -0,0 +1,68 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) + +require 'rubygems' +require 'softlayer_api' +require 'rspec' + +describe SoftLayer::ServerFirewall do + describe "firewall rules bypass" do + let(:mock_client) { + mock_client = SoftLayer::Client.new(:username => "fake_user", :api_key => "BADKEY") + } + + it "responds to the method change_routing_bypass!" do + mock_firewall = SoftLayer::ServerFirewall.new("not really a client", { "id" => 12345 }) + expect(mock_firewall).to respond_to(:change_rules_bypass!) + end + + it "accepts :apply_firewall_rules" do + mock_firewall = SoftLayer::ServerFirewall.new(mock_client, {"id" => 12345}) + allow(mock_firewall).to receive(:rules) { {} } + + firewall_update_service = mock_client[:Network_Firewall_Update_Request] + + expect(firewall_update_service).to receive(:call_softlayer_api_with_params) do |method, parameters, arguments| + expect(arguments[0]['bypassFlag']).to be(false) + end + + mock_firewall.change_rules_bypass!(:apply_firewall_rules) + end + + it "accepts :bypass_firewall_rules!" do + mock_firewall = SoftLayer::ServerFirewall.new(mock_client, {"id" => 12345}) + allow(mock_firewall).to receive(:rules) { {} } + + firewall_update_service = mock_client[:Network_Firewall_Update_Request] + expect(firewall_update_service).to receive(:call_softlayer_api_with_params) do |method, parameters, arguments| + expect(arguments[0]['bypassFlag']).to be(true) + end + + mock_firewall.change_rules_bypass!(:bypass_firewall_rules) + end + + it "rejects other parameters (particularly true and false)" do + mock_firewall = SoftLayer::ServerFirewall.new("not really a client", { "id" => 12345 }) + allow(mock_firewall).to receive(:rules) { {} } + + firewall_update_service = mock_client[:Network_Firewall_Update_Request] + + allow(firewall_update_service).to receive(:call_softlayer_api_with_params) + + expect{ mock_firewall.change_rules_bypass!(true) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(false) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(:route_around_firewall) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(:route_through_firewall) }.to raise_error + expect{ mock_firewall.change_rules_bypass!("apply_firewall_rules") }.to raise_error + expect{ mock_firewall.change_rules_bypass!("bypass_firewall_rules") }.to raise_error + expect{ mock_firewall.change_rules_bypass!(nil) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(1) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(0) }.to raise_error + end + end +end \ No newline at end of file diff --git a/spec/Server_spec.rb b/spec/Server_spec.rb index 7276773..d40d5b9 100644 --- a/spec/Server_spec.rb +++ b/spec/Server_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -29,12 +13,6 @@ describe SoftLayer::Server do it "is an abstract base class" do mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "DEADBEEFBADF00D") - allow(mock_client).to receive(:[]) do |service_name| - service = mock_client.service_named(service_name) - allow(service).to receive(:call_softlayer_api_with_params) - service - end - expect { SoftLayer::Server.new(mock_client, { "id" => 12345 }) }.to raise_error end end \ No newline at end of file diff --git a/spec/Service_spec.rb b/spec/Service_spec.rb index b2b8fff..e966426 100644 --- a/spec/Service_spec.rb +++ b/spec/Service_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -84,7 +68,7 @@ before(:each) do SoftLayer::Service.send(:public, :xmlrpc_client) end - + it "Constructs an XMLRPC client with a given timeout value based on the timeout of the client" do client = SoftLayer::Client.new(:username => 'fake_user', :api_key => 'fake_key', :timeout => 60) ticket_service = client[:Ticket] @@ -100,7 +84,7 @@ describe "#missing_method" do it "translates unknown methods into api calls" do - expect(service).to receive(:call_softlayer_api_with_params).with(:getOpenTickets, nil, ["marshmallow"]) + expect(service).to receive(:call_softlayer_api_with_params).with(:getOpenTickets, nil, ['marshmallow']) response = service.getOpenTickets("marshmallow") end end @@ -176,8 +160,9 @@ describe "#object_filter" do let (:object_filter) do - object_filter = SoftLayer::ObjectFilter.new() - object_filter["key"] = "value" + object_filter = SoftLayer::ObjectFilter.new() do |filter| + filter.set_criteria_for_key_path("key", "value") + end object_filter end @@ -185,12 +170,12 @@ parameter_filter = service.object_filter(object_filter) expect(parameter_filter).to_not be_nil expect(parameter_filter.target).to eq service - expect(parameter_filter.server_object_filter).to eq object_filter + expect(parameter_filter.server_object_filter).to eq object_filter.to_h end it "passes an object filter through to an API call" do expect(service).to receive(:call_softlayer_api_with_params).with(:getObject, an_instance_of(SoftLayer::APIParameterFilter),[]) do |method_name, parameters, args| - expect(parameters.server_object_filter).to eq object_filter + expect(parameters.server_object_filter).to eq object_filter.to_h end service.object_filter(object_filter).getObject diff --git a/spec/Ticket_spec.rb b/spec/Ticket_spec.rb index efe35cd..862569d 100644 --- a/spec/Ticket_spec.rb +++ b/spec/Ticket_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -33,23 +17,12 @@ SoftLayer::Ticket.instance_eval { @ticket_subjects = nil } end - it "fetches a list of open tickets" do - mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "fake_api_key") - account_service = mock_client["Account"] - - expect(account_service).to receive(:call_softlayer_api_with_params).with(:getOpenTickets, instance_of(SoftLayer::APIParameterFilter),[]) do - fixture_from_json("test_tickets") - end - - SoftLayer::Ticket.open_tickets(:client => mock_client) - end - it "retrieves ticket subjects from API once" do fakeTicketSubjects = fixture_from_json("ticket_subjects") mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key=> 'fakekey') allow(mock_client).to receive(:[]) do |service_name| - expect(service_name).to eq "Ticket_Subject" + expect(service_name).to eq :Ticket_Subject mock_service = SoftLayer::Service.new("SoftLayer_Ticket_Subject", :client => mock_client) expect(mock_service).to receive(:getAllObjects).once.and_return(fakeTicketSubjects) @@ -75,7 +48,7 @@ mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key=> 'fakekey') allow(mock_client).to receive(:[]) do |service_name| - expect(service_name).to eq "Ticket" + expect(service_name).to eq :Ticket mock_service = SoftLayer::Service.new("SoftLayer_Ticket", :client => mock_client) # mock out call_softlayer_api_with_params so the service doesn't actually try to diff --git a/spec/VLANFirewall_spec.rb b/spec/VLANFirewall_spec.rb new file mode 100644 index 0000000..913460f --- /dev/null +++ b/spec/VLANFirewall_spec.rb @@ -0,0 +1,122 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) + +require 'rubygems' +require 'softlayer_api' +require 'rspec' + +describe SoftLayer::VLANFirewall do + it "should have a class representing a firewall" do + expect{ SoftLayer::VLANFirewall.new("not really a client", { "id" => 12345 }) }.to_not raise_error + end + + describe "firewall rules bypass" do + let(:mock_client) { + mock_client = SoftLayer::Client.new(:username => "fake_user", :api_key => "BADKEY") + } + + it "responds to the method change_routing_bypass!" do + mock_firewall = SoftLayer::VLANFirewall.new("not really a client", { "id" => 12345 }) + expect(mock_firewall).to respond_to(:change_rules_bypass!) + end + + it "accepts :apply_firewall_rules" do + mock_firewall = SoftLayer::VLANFirewall.new(mock_client, { "id" => 12345, 'networkVlanFirewall' => {'id' => 67890}}) + allow(mock_firewall).to receive(:rules_ACL_id) { 0 } + allow(mock_firewall).to receive(:rules) { {} } + + firewall_update_service = mock_client[:Network_Firewall_Update_Request] + + expect(firewall_update_service).to receive(:call_softlayer_api_with_params) do |method, parameters, arguments| + expect(arguments[0]['bypassFlag']).to be(false) + end + + mock_firewall.change_rules_bypass!(:apply_firewall_rules) + end + + it "accepts :bypass_firewall_rules!" do + mock_firewall = SoftLayer::VLANFirewall.new(mock_client, { "id" => 12345, 'networkVlanFirewall' => {'id' => 67890}}) + allow(mock_firewall).to receive(:rules_ACL_id) { 0 } + allow(mock_firewall).to receive(:rules) { {} } + + firewall_update_service = mock_client[:Network_Firewall_Update_Request] + expect(firewall_update_service).to receive(:call_softlayer_api_with_params) do |method, parameters, arguments| + expect(arguments[0]['bypassFlag']).to be(true) + end + + mock_firewall.change_rules_bypass!(:bypass_firewall_rules) + end + + it "rejects other parameters (particularly true and false)" do + mock_firewall = SoftLayer::VLANFirewall.new("not really a client", { "id" => 12345 }) + allow(mock_firewall).to receive(:rules) { {} } + + firewall_update_service = mock_client[:Network_Firewall_Update_Request] + + allow(firewall_update_service).to receive(:call_softlayer_api_with_params) + + expect{ mock_firewall.change_rules_bypass!(true) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(false) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(:route_around_firewall) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(:route_through_firewall) }.to raise_error + expect{ mock_firewall.change_rules_bypass!("apply_firewall_rules") }.to raise_error + expect{ mock_firewall.change_rules_bypass!("bypass_firewall_rules") }.to raise_error + expect{ mock_firewall.change_rules_bypass!(nil) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(1) }.to raise_error + expect{ mock_firewall.change_rules_bypass!(0) }.to raise_error + end + end + + describe "firewall routing changes" do + let(:mock_client) { + mock_client = SoftLayer::Client.new(:username => "fake_user", :api_key => "BADKEY") + } + + it "responds to the method change_routing_bypass!" do + mock_firewall = SoftLayer::VLANFirewall.new("not really a client", { "id" => 12345 }) + expect(mock_firewall).to respond_to(:change_routing_bypass!) + end + + it "accepts :route_through_firewall" do + mock_firewall = SoftLayer::VLANFirewall.new(mock_client, { "id" => 12345, 'networkVlanFirewall' => {'id' => 67890}}) + allow(mock_firewall).to receive(:rules_ACL_id) { 0 } + vlan_firewall_service = mock_client[:Network_Vlan_Firewall] + + expect(vlan_firewall_service).to receive(:call_softlayer_api_with_params).with(:updateRouteBypass,an_instance_of(SoftLayer::APIParameterFilter),[false]) + mock_firewall.change_routing_bypass!(:route_through_firewall) + end + + it "accepts :route_around_firewall" do + mock_firewall = SoftLayer::VLANFirewall.new(mock_client, { "id" => 12345, 'networkVlanFirewall' => {'id' => 67890}}) + allow(mock_firewall).to receive(:rules_ACL_id) { 0 } + vlan_firewall_service = mock_client[:Network_Vlan_Firewall] + + expect(vlan_firewall_service).to receive(:call_softlayer_api_with_params).with(:updateRouteBypass,an_instance_of(SoftLayer::APIParameterFilter),[true]) + mock_firewall.change_routing_bypass!(:route_around_firewall) + end + + it "rejects other parameters (particularly true and false)" do + mock_firewall = SoftLayer::VLANFirewall.new("not really a client", { "id" => 12345 }) + allow(mock_firewall).to receive(:rules_ACL_id) { 0 } + + vlan_firewall_service = mock_client[:Network_Vlan_Firewall] + + allow(vlan_firewall_service).to receive(:call_softlayer_api_with_params) + + expect{ mock_firewall.change_routing_bypass!(true) }.to raise_error + expect{ mock_firewall.change_routing_bypass!(false) }.to raise_error + expect{ mock_firewall.change_routing_bypass!(:apply_firewall_rules) }.to raise_error + expect{ mock_firewall.change_routing_bypass!(:bypass_firewall_rules) }.to raise_error + expect{ mock_firewall.change_routing_bypass!("route_around_firewall") }.to raise_error + expect{ mock_firewall.change_routing_bypass!("route_through_firewall") }.to raise_error + expect{ mock_firewall.change_routing_bypass!(nil) }.to raise_error + expect{ mock_firewall.change_routing_bypass!(1) }.to raise_error + expect{ mock_firewall.change_routing_bypass!(0) }.to raise_error + end + end +end \ No newline at end of file diff --git a/spec/VirtualServerOrder_spec.rb b/spec/VirtualServerOrder_spec.rb index d50995f..b940266 100644 --- a/spec/VirtualServerOrder_spec.rb +++ b/spec/VirtualServerOrder_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -50,50 +34,53 @@ end it "places its :datacenter attribute into the order template" do - expect(subject.virtual_guest_template["datacenter"]).to be_nil - subject.datacenter = "dal05" - expect(subject.virtual_guest_template["datacenter"]).to eq({ "name" => "dal05" }) + client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') + expect(subject.virtual_guest_template['datacenter']).to be_nil + subject.datacenter = SoftLayer::Datacenter.new(client, 'id' => 42, 'name' => "dal05") + expect(subject.virtual_guest_template['datacenter']).to eq({ "name" => "dal05" }) end it "places its :hostname attribute into the order template" do - expect(subject.virtual_guest_template["hostname"]).to be_nil + expect(subject.virtual_guest_template['hostname']).to be_nil subject.hostname = "testhostname" - expect(subject.virtual_guest_template["hostname"]).to eq "testhostname" + expect(subject.virtual_guest_template['hostname']).to eq "testhostname" end it "places its :domain attribute into the order template" do - expect(subject.virtual_guest_template["domain"]).to be_nil + expect(subject.virtual_guest_template['domain']).to be_nil subject.domain = "softlayer.com" - expect(subject.virtual_guest_template["domain"]).to eq "softlayer.com" + expect(subject.virtual_guest_template['domain']).to eq "softlayer.com" end it "places its :cores attribute into the order template as startCpus" do subject.cores = 4 - expect(subject.virtual_guest_template["startCpus"]).to eq 4 + expect(subject.virtual_guest_template['startCpus']).to eq 4 end it "places the MB value of the :memory attrbute in the template as maxMemory" do subject.memory = 4 - expect(subject.virtual_guest_template["maxMemory"]).to eq 4096 + expect(subject.virtual_guest_template['maxMemory']).to eq 4096 end it "places an OS identifier into the order template as the operatingSystemReferenceCode" do - expect(subject.virtual_guest_template["operatingSystemReferenceCode"]).to be_nil + expect(subject.virtual_guest_template['operatingSystemReferenceCode']).to be_nil subject.os_reference_code = 'UBUNTU_12_64' expect(subject.virtual_guest_template['operatingSystemReferenceCode']).to eq 'UBUNTU_12_64' end it "places an image template global identifier in the template as blockDeviceTemplateGroup.globalIdentifier" do - expect(subject.virtual_guest_template["blockDeviceTemplateGroup"]).to be_nil - subject.image_global_id = "12345-abcd-eatatjoes" + client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') + expect(subject.virtual_guest_template['blockDeviceTemplateGroup']).to be_nil + subject.image_template = SoftLayer::ImageTemplate.new(client, 'id' => 42, 'globalIdentifier' => '12345-abcd-eatatjoes'); expect(subject.virtual_guest_template['blockDeviceTemplateGroup']).to eq({'globalIdentifier' => '12345-abcd-eatatjoes'}) end - it "allows an image global id to override an os reference code when both are provided" do - expect(subject.virtual_guest_template["blockDeviceTemplateGroup"]).to be_nil - expect(subject.virtual_guest_template["operatingSystemReferenceCode"]).to be_nil + it "allows an image template to override an os reference code when both are provided" do + expect(subject.virtual_guest_template['blockDeviceTemplateGroup']).to be_nil + expect(subject.virtual_guest_template['operatingSystemReferenceCode']).to be_nil - subject.image_global_id = "12345-abcd-eatatjoes" + client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') + subject.image_template = SoftLayer::ImageTemplate.new(client, 'id' => 42, 'globalIdentifier' => '12345-abcd-eatatjoes'); subject.os_reference_code = 'UBUNTU_12_64' expect(subject.virtual_guest_template['blockDeviceTemplateGroup']).to eq({'globalIdentifier' => '12345-abcd-eatatjoes'}) @@ -102,50 +89,50 @@ it "places the attribute :hourly into the template as hourlyBillingFlag converting the value to a boolean constant" do # note, we don't want the flag to be nil we want it to be eotjer false or true - expect(subject.virtual_guest_template["hourlyBillingFlag"]).to be(false) + expect(subject.virtual_guest_template['hourlyBillingFlag']).to be(false) subject.hourly = true - expect(subject.virtual_guest_template["hourlyBillingFlag"]).to be(true) + expect(subject.virtual_guest_template['hourlyBillingFlag']).to be(true) subject.hourly = false - expect(subject.virtual_guest_template["hourlyBillingFlag"]).to be(false) + expect(subject.virtual_guest_template['hourlyBillingFlag']).to be(false) end it "places the attribute :use_local_disk in the template as the localDiskFlag" do # note, we don't want the flag to be nil we want it to be false or true - expect(subject.virtual_guest_template["localDiskFlag"]).to be(false) + expect(subject.virtual_guest_template['localDiskFlag']).to be(false) subject.use_local_disk = true - expect(subject.virtual_guest_template["localDiskFlag"]).to be(true) + expect(subject.virtual_guest_template['localDiskFlag']).to be(true) subject.use_local_disk = false - expect(subject.virtual_guest_template["localDiskFlag"]).to be(false) + expect(subject.virtual_guest_template['localDiskFlag']).to be(false) end it "places the attribute :dedicated_host_only in the template as dedicatedAccountHostOnlyFlag" do - expect(subject.virtual_guest_template["dedicatedAccountHostOnlyFlag"]).to be_nil + expect(subject.virtual_guest_template['dedicatedAccountHostOnlyFlag']).to be_nil subject.dedicated_host_only = true - expect(subject.virtual_guest_template["dedicatedAccountHostOnlyFlag"]).to be(true) + expect(subject.virtual_guest_template['dedicatedAccountHostOnlyFlag']).to be(true) end it "puts the public VLAN id into an order template as primaryNetworkComponent.networkVlan.id" do - expect(subject.virtual_guest_template["primaryNetworkComponent"]).to be_nil + expect(subject.virtual_guest_template['primaryNetworkComponent']).to be_nil subject.public_vlan_id = 12345 - expect(subject.virtual_guest_template["primaryNetworkComponent"]).to eq({ "networkVlan" => { "id" => 12345 } }) + expect(subject.virtual_guest_template['primaryNetworkComponent']).to eq({ "networkVlan" => { "id" => 12345 } }) end it "puts the private VLAN id into an order template as primaryBackendNetworkComponent.networkVlan.id" do - expect(subject.virtual_guest_template["primaryBackendNetworkComponent"]).to be_nil + expect(subject.virtual_guest_template['primaryBackendNetworkComponent']).to be_nil subject.private_vlan_id = 12345 - expect(subject.virtual_guest_template["primaryBackendNetworkComponent"]).to eq({ "networkVlan" => { "id" => 12345 } }) + expect(subject.virtual_guest_template['primaryBackendNetworkComponent']).to eq({ "networkVlan" => { "id" => 12345 } }) end it "sets up disks in the order template as blockDevices" do - expect(subject.virtual_guest_template["blockDevices"]).to be_nil + expect(subject.virtual_guest_template['blockDevices']).to be_nil subject.disks = [2, 25, 50] # note that device id 1 should be skipped as SoftLayer reserves that id for OS swap space. - expect(subject.virtual_guest_template["blockDevices"]).to eq [ + expect(subject.virtual_guest_template['blockDevices']).to eq [ {"device"=>"0", "diskImage"=>{"capacity"=>2}}, {"device"=>"2", "diskImage"=>{"capacity"=>25}}, {"device"=>"3", "diskImage"=>{"capacity"=>50}} @@ -153,37 +140,37 @@ end it "puts the :ssh_key_ids in the template as sshKeys and breaks out the ids into objects" do - expect(subject.virtual_guest_template["sshKeys"]).to be_nil + expect(subject.virtual_guest_template['sshKeys']).to be_nil subject.ssh_key_ids = [123, 456, 789] expect(subject.virtual_guest_template['sshKeys']).to eq [{'id' => 123}, {'id' => 456}, {'id' => 789}] end it "puts the :provision_script_URI property into the template as postInstallScriptUri" do - expect(subject.virtual_guest_template["postInstallScriptUri"]).to be_nil + expect(subject.virtual_guest_template['postInstallScriptUri']).to be_nil subject.provision_script_URI = 'http:/provisionhome.mydomain.com/fancyscript.sh' expect(subject.virtual_guest_template['postInstallScriptUri']).to eq 'http:/provisionhome.mydomain.com/fancyscript.sh' end it "accepts URI objects for the provision script URI" do - expect(subject.virtual_guest_template["postInstallScriptUri"]).to be_nil + expect(subject.virtual_guest_template['postInstallScriptUri']).to be_nil subject.provision_script_URI = URI.parse('http:/provisionhome.mydomain.com/fancyscript.sh') expect(subject.virtual_guest_template['postInstallScriptUri']).to eq 'http:/provisionhome.mydomain.com/fancyscript.sh' end it "places the private_network_only attribute in the template as privateNetworkOnlyFlag" do - expect(subject.virtual_guest_template["privateNetworkOnlyFlag"]).to be_nil + expect(subject.virtual_guest_template['privateNetworkOnlyFlag']).to be_nil subject.private_network_only = true - expect(subject.virtual_guest_template["privateNetworkOnlyFlag"]).to be(true) + expect(subject.virtual_guest_template['privateNetworkOnlyFlag']).to be(true) end it "puts the user metadata string into the template as userData" do - expect(subject.virtual_guest_template["userData"]).to be_nil + expect(subject.virtual_guest_template['userData']).to be_nil subject.user_metadata = "MetadataValue" expect(subject.virtual_guest_template['userData']).to eq [{'value' => 'MetadataValue'}] end it "puts the max_port_speed attribute into the template as networkComponents.maxSpeed" do - expect(subject.virtual_guest_template["networkComponents"]).to be_nil + expect(subject.virtual_guest_template['networkComponents']).to be_nil subject.max_port_speed = 1000 expect(subject.virtual_guest_template['networkComponents']).to eq [{'maxSpeed' => 1000}] end @@ -197,7 +184,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - virtual_guest_service = client["Virtual_Guest"] + virtual_guest_service = client[:Virtual_Guest] allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) expect(virtual_guest_service).to receive(:generateOrderTemplate).with(test_order.virtual_guest_template) @@ -213,7 +200,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - virtual_guest_service = client["Virtual_Guest"] + virtual_guest_service = client[:Virtual_Guest] allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) expect(virtual_guest_service).to receive(:createObject).with(test_order.virtual_guest_template) @@ -229,7 +216,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - virtual_guest_service = client["Virtual_Guest"] + virtual_guest_service = client[:Virtual_Guest] allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) substituted_order_template = { 'aFake' => 'andBogusOrderTemplate' } @@ -246,7 +233,7 @@ test_order.hostname = "ruby-client-test" test_order.domain = "kitchentools.com" - virtual_guest_service = client["Virtual_Guest"] + virtual_guest_service = client[:Virtual_Guest] allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) substituted_order_template = { 'aFake' => 'andBogusOrderTemplate' } @@ -257,7 +244,7 @@ describe "methods returning available options for attributes" do let (:client) do client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') - virtual_guest_service = client["Virtual_Guest"] + virtual_guest_service = client[:Virtual_Guest] allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) fake_options = fixture_from_json("Virtual_Guest_createObjectOptions") @@ -278,7 +265,10 @@ end it "transmogrifies the datacenter options for the cores attribute" do - expect(SoftLayer::VirtualServerOrder.datacenter_options(client)).to eq ["ams01", "dal01", "dal05", "dal06", "sea01", "sjc01", "sng01", "wdc01"] + datacenter_options = SoftLayer::VirtualServerOrder.datacenter_options(client) + datacenter_names = datacenter_options.map { |datacenter| datacenter.name } + + expect(datacenter_names.sort).to eq ["ams01", "dal01", "dal05", "dal06", "sea01", "sjc01", "sng01", "wdc01"] end it "transmogrifies the processor options for the cores attribute" do diff --git a/spec/VirtualServerUpgradeOrder_spec.rb b/spec/VirtualServerUpgradeOrder_spec.rb new file mode 100644 index 0000000..9a8bede --- /dev/null +++ b/spec/VirtualServerUpgradeOrder_spec.rb @@ -0,0 +1,154 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) + +require 'rubygems' +require 'softlayer_api' +require 'rspec' + +describe SoftLayer::VirtualServerUpgradeOrder do + before(:each) do + SoftLayer::VirtualServerUpgradeOrder.send(:public, *SoftLayer::VirtualServerUpgradeOrder.private_instance_methods) + end + + let(:test_virtual_server) do + mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "DEADBEEFBADF00D") + virtual_guest_service = mock_client[:Virtual_Guest] + allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) do |api_method, parameters, api_arguments| + api_return = nil + + case api_method + when :getUpgradeItemPrices + api_return = fixture_from_json('virtual_server_upgrade_options') + else + fail "Unexpected call to the SoftLayer_Virtual_Guest service" + end + + api_return + end + + test_servers = fixture_from_json('test_virtual_servers') + SoftLayer::VirtualServer.new(mock_client, test_servers.first) + end + + it "requires a virtual server when initialized" do + expect { SoftLayer::VirtualServerUpgradeOrder.new(nil) }.to raise_error + expect { SoftLayer::VirtualServerUpgradeOrder.new("foo") }.to raise_error + expect { SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) }.to_not raise_error + end + + it "initializes with none of the upgrades specified" do + upgrade_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + expect(upgrade_order.cores == nil) + expect(upgrade_order.ram == nil) + expect(upgrade_order.max_port_speed == nil) + expect(upgrade_order.upgrade_at == nil) + end + + it "identifies what options are available for upgrading the number of cores" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + expect(sample_order.core_options).to eq [1, 2, 4, 8, 12, 16] + end + + it "identifies what options are available for upgrading ram" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + expect(sample_order.memory_options).to eq [1, 2, 4, 6, 8, 12, 16, 32, 48, 64] + end + + it "identifies what options are available for upgrading max port speed" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + expect(sample_order.max_port_speed_options).to eq [10, 100, 1000] + end + + it "places the number of cores asked for into the order template" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + sample_order.core_options.each do |num_cores| + sample_order.cores = num_cores + test_template = sample_order.order_object + expect(sample_order.order_object['prices'].length).to be(1) + + item = sample_order._item_price_with_capacity("guest_core", num_cores) + expect(test_template['prices'].first['id']).to eq item['id'] + end + end + + it "places the amount of RAM asked for into the order template" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + sample_order.memory_options.each do |ram_in_GB| + sample_order.ram = ram_in_GB + test_template = sample_order.order_object + expect(sample_order.order_object['prices'].length).to be(1) + + item = sample_order._item_price_with_capacity("ram", ram_in_GB) + expect(test_template['prices'].first['id']).to eq item['id'] + end + end + + it "places the port speed asked for into the order template" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + sample_order.max_port_speed_options.each do |port_speed| + sample_order.max_port_speed = port_speed + test_template = sample_order.order_object + expect(sample_order.order_object['prices'].length).to be(1) + + item = sample_order._item_price_with_capacity("port_speed", port_speed) + expect(test_template['prices'].first['id']).to eq item['id'] + end + end + + it "adds the default maintenance window of 'now' if none is given" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + sample_order.cores = 2 + test_template = sample_order.order_object + + expect(sample_order.order_object['properties'].first['name']).to eq('MAINTENANCE_WINDOW') + + time_string = sample_order.order_object['properties'].first['value'] + maintenance_time = Time.iso8601(time_string) + + expect((Time.now - maintenance_time) <= 1.0).to be(true) + end + + it "adds the appointed maintenance window one is given" do + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + sample_order.cores = 2 + + upgrade_time = Time.now + 3600 # in an hour + sample_order.upgrade_at = upgrade_time + + test_template = sample_order.order_object + + expect(sample_order.order_object['properties'].first['name']).to eq('MAINTENANCE_WINDOW') + + time_string = sample_order.order_object['properties'].first['value'] + expect(time_string).to eq upgrade_time.iso8601 + end + + it "verifies product orders" do + product_order_service = test_virtual_server.softlayer_client[:Product_Order] + + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + sample_order.cores = 2 + + order_object = sample_order.order_object + expect(product_order_service).to receive(:call_softlayer_api_with_params).with(:verifyOrder, anything, [order_object]) + + sample_order.verify() + end + + it "places product orders" do + product_order_service = test_virtual_server.softlayer_client[:Product_Order] + + sample_order = SoftLayer::VirtualServerUpgradeOrder.new(test_virtual_server) + sample_order.cores = 2 + + order_object = sample_order.order_object + expect(product_order_service).to receive(:call_softlayer_api_with_params).with(:placeOrder, anything, [order_object]) + + sample_order.place_order!() + end +end \ No newline at end of file diff --git a/spec/VirtualServer_spec.rb b/spec/VirtualServer_spec.rb index c8cda3f..4ffe1d8 100644 --- a/spec/VirtualServer_spec.rb +++ b/spec/VirtualServer_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -68,12 +52,12 @@ it_behaves_like "server with mutable hostname" do let (:server) { sample_server } end - + describe "component upgrades" do let(:mock_client) do mock_client = SoftLayer::Client.new(:username => "fakeuser", :api_key => "DEADBEEFBADF00D") virtual_guest_service = mock_client[:Virtual_Guest] - + allow(virtual_guest_service).to receive(:call_softlayer_api_with_params) do |api_method, parameters, api_arguments| api_return = nil @@ -83,7 +67,7 @@ else fail "Unexpected call to the SoftLayer_Virtual_Guest service" end - + api_return end @@ -98,30 +82,5 @@ expect(mock_client[:Virtual_Guest]).to_not receive(:call_softlayer_api_with_params) fake_virtual_server.upgrade_options end - - describe "individual component upgrades" do - before(:each) do - expect(mock_client[:Product_Order]).to receive(:call_softlayer_api_with_params) do |api_method, parameters, api_arguments| - expect(api_method).to be(:placeOrder) - expect(parameters).to be_nil - expect(api_method).to_not be_empty - end - end - - it "upgrades cores" do - fake_virtual_server = SoftLayer::VirtualServer.new(mock_client, {"id" => 12345}) - fake_virtual_server.upgrade_cores!(8) - end - - it "upgrades ram" do - fake_virtual_server = SoftLayer::VirtualServer.new(mock_client, {"id" => 12345}) - fake_virtual_server.upgrade_RAM!(4) - end - - it "upgrades max port speed" do - fake_virtual_server = SoftLayer::VirtualServer.new(mock_client, {"id" => 12345}) - fake_virtual_server.upgrade_max_port_speed!(100) - end - end # individual component upgrades - end + end end \ No newline at end of file diff --git a/spec/XMLRPC_Convert_spec.rb b/spec/XMLRPC_Convert_spec.rb index f25f1e3..8aae24f 100644 --- a/spec/XMLRPC_Convert_spec.rb +++ b/spec/XMLRPC_Convert_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) @@ -32,7 +16,7 @@ expect { XMLRPC::Convert.fault(fault_hash) }.not_to raise_error exception = XMLRPC::Convert.fault(fault_hash) expect(exception).to be_kind_of(XMLRPC::FaultException) - expect(exception.faultCode).to eq(fault_hash["faultCode"]) - expect(exception.faultString).to eq(fault_hash["faultString"]) + expect(exception.faultCode).to eq(fault_hash['faultCode']) + expect(exception.faultString).to eq(fault_hash['faultString']) end end diff --git a/spec/fixtures/datacenter_locations.json b/spec/fixtures/datacenter_locations.json new file mode 100644 index 0000000..294e4d4 --- /dev/null +++ b/spec/fixtures/datacenter_locations.json @@ -0,0 +1 @@ +[{"id":265592,"longName":"Amsterdam 1","name":"ams01"},{"id":358698,"longName":"Ashburn 3","name":"wdc03"},{"id":3,"longName":"Dallas 1","name":"dal01"},{"id":154770,"longName":"Dallas 2","name":"dal02"},{"id":167092,"longName":"Dallas 4","name":"dal04"},{"id":138124,"longName":"Dallas 5","name":"dal05"},{"id":154820,"longName":"Dallas 6","name":"dal06"},{"id":142776,"longName":"Dallas 7","name":"dal07"},{"id":352494,"longName":"Hong Kong 2","name":"hkg02"},{"id":142775,"longName":"Houston 2","name":"hou02"},{"id":358694,"longName":"London 2","name":"lon02"},{"id":168642,"longName":"San Jose 1","name":"sjc01"},{"id":18171,"longName":"Seattle","name":"sea01"},{"id":224092,"longName":"Singapore 1","name":"sng01"},{"id":448994,"longName":"Toronto 1","name":"tor01"},{"id":37473,"longName":"Washington 1","name":"wdc01"}] \ No newline at end of file diff --git a/spec/object_mask_helpers_spec.rb b/spec/object_mask_helpers_spec.rb index ea4a2ca..b0b1a4a 100644 --- a/spec/object_mask_helpers_spec.rb +++ b/spec/object_mask_helpers_spec.rb @@ -1,24 +1,8 @@ -# +#-- # Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# +# For licensing information see the LICENSE.md file in the project root. +#++ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "../lib"))