diff --git a/.gitignore b/.gitignore index 3b18490..cbc701c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ log/ internal_examples/ doc/ -Gemfile.lock \ No newline at end of file +coverage/ +Gemfile.lock +vendor/ +pkg/ diff --git a/.travis.yml b/.travis.yml index d3ceb36..4512c94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: ruby rvm: - - "2.1.2" - - "2.1.1" + - "2.3.0" + - "2.2.4" + - "2.1.8" - "2.0.0" - - "1.9.3" - - "1.9.2" - jruby-19mode diff --git a/CHANGELOG.textile b/CHANGELOG.textile index 1b4fb44..c8b5f36 100644 --- a/CHANGELOG.textile +++ b/CHANGELOG.textile @@ -1,9 +1,41 @@ -*2.2* +*3.2.3* +* Support for profiles. +* Support for HVM. + + +*3.2.2* +* Fix NoMethodError when getting the datacenters out of an ImageTemplate and the value was nil. +* Fix NoMethodError when getting public and private images with a result limit of 1. + +*3.2.1* +* Fix a crashing issue where a Bare Metal server order tried to retrieve the hardware ordered before it has been provisioned. + +*3.2* +* Add password-based authentication with `SoftLayer::Client.with_password(username: '...', password: '...', ...)`. + +*3.1.0* +* Many SoftLayer Attributes are now shadowed with snake_case names. The old CamelCase versions are deprecated and will be removed in the next major release +* New functionality related to network monitoring has been added + +*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) + +*2.2* +* Added the ability to set a timout for network requests. The timeout is given when a client is created by passing the :timeout hash parameter when creating a client. The value of the parameter is an integer number of seconds. +* Fixed a bug in VirtualServer#capture_image *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..46baf09 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,11 @@ This will create a new folder named `doc` inside of the project source and popul This software is written by the SoftLayer Development Team [sldn@softlayer.com](mailto:sldn@softlayer.com). -Please join us in the [SoftLayer Developer Network forums](http://forums.softlayer.com/forum/softlayer-developer-network) +Please join us at [Stack Overflow](https://stackoverflow.com/). Stack Overflow Best Practices Tip: Tag your posts with “SoftLayer” so our team can easily find your post. + +# 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 diff --git a/doc_src/Contribution Guide.md b/doc_src/ContributionGuide.md similarity index 97% rename from doc_src/Contribution Guide.md rename to doc_src/ContributionGuide.md index 699bcf4..6089dec 100644 --- a/doc_src/Contribution Guide.md +++ b/doc_src/ContributionGuide.md @@ -6,8 +6,8 @@ The `softlayer_api` Ruby Gem is an open source project and the developers who us Contributions to the softlayer-ruby project require the submission of a contributer license agreement. Individual contributers should review and -complete the [CLA](./cla-individual.md). Contributions made of behalf of a -company/employer will necessitate the completion of the [CCLA](./cla-corporate.md). +complete the [CLA](./cla-individual_md.html). Contributions made of behalf of a +company/employer will necessitate the completion of the [CCLA](./cla-corporate_md.html). # Requesting Changes @@ -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. @@ -64,7 +64,7 @@ gems are built into the `pkg` directory and will have a name of the form `softla You can install your modified gem to your system with the gem command: - $ bundle gem install pkg/softlayer_api-.gem + $ bundle exec gem install pkg/softlayer_api-.gem (don't forget to substitute the version you are installing where the `` tag appears in the command line above) @@ -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 @@ -138,4 +138,3 @@ If you intend to offer new models, please carefully review the Model Layer docum # Submitting Changes Contributions are made to the `softlayer_api` Gem by submitting a pull-request on GitHub. The community will review pull requests and offer constructive advice on improvements. The determination on whether a pull-request will be accepted into the gem is made at the sole discretion of SoftLayer with the wise counsel of the community. - diff --git a/doc_src/Foundation.md b/doc_src/Foundation.md index d8f62a3..290c522 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. @@ -139,22 +139,34 @@ The first argument to the `result_limit` helper is the index in the array of the Only one call to the `result_limit` helper should be included in any calling sequence. -## Object Filters - -Object Filters ask the server to filter the result set using a set of criteria before returning its results. Unfortunately, at the time of this writing, constructing object filters is not a well documented process. Luckily, the `softlayer_api` Ruby Gem, offers some convenience functionality to help you create simple object filters. If you want help crafting a particular object filter, we suggest you ask in the [SLDN Forums](https://forums.softlayer.com/forum/softlayer-developer-network) for assistance. - -As an example, suppose you wished to obtain a list of all the virtual servers on an account that were in the domain kitchentools.com. To get the list of virtual servers on an account you would use the `SoftLayer_Account` service and call the `getVirtualGuests` method. The filter we wish to apply is based on the `domain` property of the virtual servers being returned. That code would look like this: - - filter = SoftLayer::ObjectFilter.build("domain", "kitchentools.com") - softlayer_client['Account'].object_filter(filter).getVirtualGuests() - -The object filter is applied using the `object_filter` service helper. This method takes a single parameter, the object filter to apply to the network API call. - -In order to get the filter we wish to apply, this example uses the `SoftLayer::ObjectFilter#build` method. The first parameter to `build` is the property that we wish our filter based on. The second parameter is a query string, in this case the query string means "objects whose property value exactly matches 'kitchentools.com'". Other query strings possible. For example, if we wanted to select the servers whose domain names end with 'tools.com' we could use the query string '*tools.com'. For more information about query strings, please see the documentation for the `SoftLayer::ObjectFilter#query_to_filter_operation` method. - -The `SoftLayer::ObjectFilter#build` routine also allows a block syntax which lets you specify the filter criteria using a very simple Domain Specific Language (DSL). Here is an example of constructing the same filter `domain` filter from the previous using the block technique: - - filter = SoftLayer::ObjectFilter.build("domain") { is("kichentools.com") } - -This filter also asks that the domain exactly match "kitchentools.com". Other matchers can be found in as the instance methods of the `SoftLayer::ObjectFilterBlockHandler` class. +## {ObjectFilters}[rdoc-ref:SoftLayer::ObjectFilter] + +\Object filters are sent along with a request to a service. The service filters +the results before returning them over the network. The softlayer_api gem can +create simple object filters. + +If you want help crafting a particular object filter, we suggest you ask in the +[SLDN Forums](https://forums.softlayer.com/forum/softlayer-developer-network) +for assistance. + +Suppose you want to obtain a list of all the virtual servers on an account that +are in the domain `kitchentools.com`. To get the list of virtual servers on an +account you would use the +{SoftLayer_Account}[http://sldn.softlayer.com/reference/services/SoftLayer_Account] +service and call the +{getVirtualGuests}[http://sldn.softlayer.com/reference/services/SoftLayer_Account/getVirtualGuests] +method. The filter we wish to apply is based on the `domain` property of the +virtual servers being returned. That code would look like this: + + filter = SoftLayer::ObjectFilter.new {|f| f.accept("virtualGuests.domain").when_it is("kitchentools.com")} + softlayer_client['Account'].object_filter(filter).getVirtualGuests() + +Note that the string to `accept` starts with the name of the service method +being called where the `get` is dropped and first letter downcased. In this +example `getVirtualGuests` becomes `virtualGuests`.The filter is applied using +SoftLayer::Service#object_filter. This method requires a filter obtained from +SoftLayer::ObjectFilter.new which takes a block that specifies filter criteria +using a Domain Specific Language (DSL). See +SoftLayer::ObjectFilterDefinitionContext for other matchers to use with +when\_it. 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..63d7901 100644 --- a/doc_src/Welcome.md +++ b/doc_src/Welcome.md @@ -1,6 +1,6 @@ # Welcome -The `softlyer_api` Ruby Gem provides a convenient way to call into the SoftLayer API from the Ruby programming language. This is accomplished using the XML-RPC interface provided by SoftLayer and the XMLRPC client built into the core Ruby language. +The `softlayer_api` Ruby Gem provides a convenient way to call into the SoftLayer API from the Ruby programming language. This is accomplished using the XML-RPC interface provided by SoftLayer and the XMLRPC client built into the core Ruby language. For more information about the SoftLayer API, and the routines and data structures it offers should visit the [SoftLayer Developer Network (SLDN) website](http://sldn.softlayer.com). @@ -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 +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) diff --git a/doc_src/cla-corporate.md b/doc_src/cla-corporate.md index a585c5d..c6d3658 100644 --- a/doc_src/cla-corporate.md +++ b/doc_src/cla-corporate.md @@ -18,7 +18,7 @@ designated employees to the Project, and to grant copyright and patent licenses thereto. If you have not already done so, please complete and sign, then scan and email a -pdf file of this Agreement to pjackson@softlayer.com. +pdf file of this Agreement to SLDNDeveloperRelations@wwpdl.vnet.ibm.com. diff --git a/doc_src/cla-individual.md b/doc_src/cla-individual.md index 3559249..a4a5254 100644 --- a/doc_src/cla-individual.md +++ b/doc_src/cla-individual.md @@ -13,7 +13,7 @@ well as the protection of IBM and its customers; it does not change your rights to use your own Contributions for any other purpose. If you have not already done so, please complete and sign, then scan and email a -pdf file of this Agreement to pjackson@softlayer.com +pdf file of this Agreement to SLDNDeveloperRelations@wwpdl.vnet.ibm.com. Please read this document carefully before signing and keep a copy for your records. 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..2e3724b 100644 --- a/examples/order_bare_metal_package.rb +++ b/examples/order_bare_metal_package.rb @@ -37,24 +37,26 @@ # a server. def tl_dr_version client = SoftLayer::Client.new( - # :username => "joecustomer" # enter your username here + # :username => "joecustomer", # enter your username here # :api_key => "feeddeadbeefbadf00d..." # enter your api key here ) # Select a package - quad_intel_package = SoftLayer::ProductPackage.package_with_id(client, 32) + product_package = SoftLayer::ProductPackage.package_with_id(251, client) # Find required Categories and fill config_options with defaults config_options = {} - required_categories = quad_intel_package.configuration.select { |category| category.required? } + required_categories = product_package.configuration.select { |category| category.required? } required_categories.each { |required_category| config_options[required_category.categoryCode] = required_category.default_option } - # Provide a value for missing config categories - config_options['server'] = 1417 # price id of Quad Processor Quad Core Intel 7420 - 2.13GHz (Dunnington) - 4 x 6MB / 8MB cache + # Provide a value for missing config categories as they don't have defaults + config_options['server'] = 50691 # Dual Intel Xeon E5-2620 v3 (12 Cores, 2.40 GHz) + config_options['ram'] = 49427 # 64 GB RAM + config_options['disk0'] = 49811 # 1.00 TB SATA # 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.location = 'sng01' + server_order = SoftLayer::BareMetalServerOrder_Package.new(product_package, client) + server_order.datacenter = SoftLayer::Datacenter.datacenter_named 'sng01', client server_order.hostname = 'sample' server_order.domain = 'softlayerapi.org' server_order.configuration_options = config_options @@ -69,7 +71,7 @@ def tl_dr_version begin client = SoftLayer::Client.new( - # :username => "joecustomer" # enter your username here + # :username => "joecustomer", # enter your username here # :api_key => "feeddeadbeefbadf00d..." # enter your api key here ) @@ -80,14 +82,14 @@ def tl_dr_version packages.each { |package| puts "#{package.id}\t#{package.name}"} # For this example, we'll assume that we've selected the a package - # with an id of 32 representing a "Quad Processor, Quad Core Intel" - quad_intel_package = SoftLayer::ProductPackage.package_with_id(32, client) + # with an id of 251 representing a "Dual E5-2600 v3 Series (12 Drives)" + product_package = SoftLayer::ProductPackage.package_with_id(251, client) # Now we need to now what ProductItemCategories are required to # configure a server in that package. This code prints out a table # of the required category codes with a description of each - puts "\nRequired Categories for '#{quad_intel_package.name}':" - required_categories = quad_intel_package.configuration.select { |category| category.required? } + puts "\nRequired Categories for '#{product_package.name}':" + required_categories = product_package.configuration.select { |category| category.required? } max_code_length = required_categories.inject(0) { |max_code_length, category| [category.categoryCode.length, max_code_length].max } printf "%#{max_code_length}s\tCategory Description\n", "Category Code" printf "%#{max_code_length}s\t--------------------\n", "-------------" @@ -96,7 +98,7 @@ def tl_dr_version # We will need to provide values for each of the required category codes in our # configuration_options. Let's see what configuration options are available for # just one of the categories... Say 'os' - os_category = quad_intel_package.category('os') + os_category = product_package.category('os') config_options = os_category.configuration_options puts "\nConfiguration options in the 'os' category:" config_options.each { |option| printf "%5s\t#{option.description}\n", option.price_id } @@ -119,21 +121,26 @@ def tl_dr_version # Regardless of the default values... we know we want the os selection we discovered above: config_options['os'] = os_config_option + # Provide a value for missing config categories as they don't have defaults + config_options['server'] = 50691 # Dual Intel Xeon E5-2620 v3 (12 Cores, 2.40 GHz) + config_options['ram'] = 49427 # 64 GB RAM + config_options['disk0'] = 49811 # 1.00 TB SATA + # And we can customize the default config by providing selections for any config categories # we are interested in config_options.merge! ({ - 'server' => 1417, # price id of Quad Processor Quad Core Intel 7420 - 2.13GHz (Dunnington) - 4 x 6MB / 8MB cache - 'port_speed' => 274 # 1 Gbps Public & Private Network Uplinks + 'port_speed' => 37220, # 1 Gbps Public & Private Network Uplinks (Unbonded) + 'bandwidth' => 50233 # 1000 GB Bandwidth }) # 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}"} + puts "\nData Centers for '#{product_package.name}':" + product_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 = SoftLayer::BareMetalServerOrder_Package.new(product_package, client) + server_order.datacenter = SoftLayer::Datacenter.datacenter_named 'sng01', client server_order.hostname = 'sample' server_order.domain = 'softlayerapi.org' server_order.configuration_options = config_options @@ -151,4 +158,3 @@ def tl_dr_version rescue Exception => exception $stderr.puts "An exception occurred while trying to complete the SoftLayer API calls #{exception}" end - 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/order_virtual_server.rb b/examples/order_virtual_server.rb index 0494743..1295af0 100644 --- a/examples/order_virtual_server.rb +++ b/examples/order_virtual_server.rb @@ -47,7 +47,7 @@ # The list will look something like ["ams01", "dal01", "dal05",... # Let's put our server in the 'dal05' (Dallas 5) datacenter - server_order.datacenter = 'dal05' + server_order.datacenter = SoftLayer::Datacenter.datacenter_named 'dal05', client # The order must know how many computing cores we want in our virtual # server. Again we can ask the class for options. The result will 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..840621f 100644 --- a/lib/softlayer/Account.rb +++ b/lib/softlayer/Account.rb @@ -1,42 +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. -# +# For licensing information see the LICENSE.md file in the project root. +#++ module SoftLayer class Account < SoftLayer::ModelBase include ::SoftLayer::DynamicAttribute + ## + # :attr_reader: company_name + # The company name of the primary contact + sl_attr :company_name, 'companyName' + ## # :attr_reader: # The company name of the primary contact + # + # DEPRECATION WARNING: This attribute is deprecated in favor of company_name + # and will be removed in the next major release. sl_attr :companyName + ## + # :attr_reader: first_name + # The given name name of the primary contact + sl_attr :first_name, 'firstName' + ## # :attr_reader: # The given name name of the primary contact + # + # DEPRECATION WARNING: This attribute is deprecated in favor of first_name + # and will be removed in the next major release. sl_attr :firstName + ## + # :attr_reader: last_name + # The surname of the primary contact + sl_attr :last_name, 'lastName' + ## # :attr_reader: # The surname of the primary contact + # + # DEPRECATION WARNING: This attribute is deprecated in favor of last_name + # and will be removed in the next major release. sl_attr :lastName ## @@ -67,18 +75,34 @@ class Account < SoftLayer::ModelBase # The country stored as part of the primary contact's address sl_attr :country + ## + # :attr_reader: postal_code + # The postal code (in the US, aka. zip code) of the primary contact's address + sl_attr :postal_code, 'postalCode' + ## # :attr_reader: # The postal code (in the US, aka. zip code) of the primary contact's address + # + # DEPRECATION WARNING: This attribute is deprecated in favor of postal_code + # and will be removed in the next major release. sl_attr :postalCode + ## + # :attr_reader: office_phone + # The office phone number listed for the primary contact + sl_attr :office_phone, 'officePhone' + ## # :attr_reader: - # The office phone nubmer listed for the primary contact + # The office phone number listed for the primary contact + # + # DEPRECATION WARNING: This attribute is deprecated in favor of office_phone + # and will be removed in the next major release. sl_attr :officePhone ## - # The Bare Metal Servers (physical hardware) associated with the + # Retrieve the Bare Metal Servers (physical hardware) associated with the # account. Unless you force these to update, they will be refreshed every # five minutes. # :call-seq: @@ -96,7 +120,121 @@ class Account < SoftLayer::ModelBase end ## - # The virtual servers (aka. CCIs or Virtual_Guests) associated with the + # Retrieve an account's master EVault user. This is only used when an account + # has an EVault service. + # :call-seq: + # evault_master_users(force_update=false) + sl_dynamic_attr :evault_master_users do |evault_users| + evault_users.should_update? do + @evault_master_users == nil + end + + evault_users.to_update do + evault_user_passwords = self.service.object_mask(AccountPassword.default_object_mask).getEvaultMasterUsers + evault_user_passwords.collect { |evault_user_password| AccountPassword.new(softlayer_client, evault_user_password) unless evault_user_password.empty? }.compact + end + end + + ## + # Retrieve an account's image templates. Unless you force + # these to update, they will be refreshed every five minutes + # :call-seq: + # image_templates(force_update=false) + 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 + + ## + # Retrieve an account's network message delivery accounts. + # :call-seq: + # network_message_delivery_accounts(force_update=false) + sl_dynamic_attr :network_message_delivery_accounts do |net_msg_deliv_accts| + net_msg_deliv_accts.should_update? do + @network_message_delivery_accounts == nil + end + + net_msg_deliv_accts.to_update do + network_message_delivery_accounts = self.service.object_mask(NetworkMessageDelivery.default_object_mask).getNetworkMessageDeliveryAccounts + network_message_delivery_accounts.collect { |net_msg_deliv_acct| NetworkMessageDelivery.new(softlayer_client, net_msg_deliv_acct) unless net_msg_deliv_acct.empty? }.compact + end + end + + ## + # Retrieve an account's network storage groups. + # :call-seq: + # network_storage_groups(force_update=false) + sl_dynamic_attr :network_storage_groups do |net_stor_groups| + net_stor_groups.should_update? do + @network_storage_groups == nil + end + + net_stor_groups.to_update do + network_storage_groups = self.service.object_mask(NetworkStorageGroup.default_object_mask).getNetworkStorageGroups + network_storage_groups.collect { |net_stor_group| NetworkStorageGroup.new(softlayer_client, net_stor_group) unless net_stor_group.empty? }.compact + end + end + + ## + # Retrieve an account's open tickets. Unless you force these + # to update, they will be refreshed every five minutes + # :call-seq: + # open_tickets(force_update=false) + 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 + + ## + # Retrieve an account's portal users. + # :call-seq: + # users(force_update=false) + sl_dynamic_attr :users do |users| + users.should_update? do + @users == nil + end + + users.to_update do + account_users = self.service.object_mask(UserCustomer.default_object_mask).getUsers + account_users.collect { |account_user| UserCustomer.new(softlayer_client, account_user) unless account_user.empty? }.compact + end + end + + ## + # Retrieve an account's virtual disk images. Unless you force + # these to update, they will be refreshed every five minutes + # :call-seq: + # virtual_disk_images(force_update=false) + sl_dynamic_attr :virtual_disk_images do |virtual_disk_images| + virtual_disk_images.should_update? do + @last_virtual_disk_images_update ||= Time.at(0) + (Time.now - @last_virtual_disk_images_update) > 5 * 60 # update every 5 minutes + end + + virtual_disk_images.to_update do + @last_virtual_disk_images_update ||= Time.now + virtual_disk_images_data = self.service.object_mask(SoftLayer::VirtualDiskImage.default_object_mask).getVirtualDiskImages + virtual_disk_images_data.collect { |virtual_disk_image| SoftLayer::VirtualDiskImage.new(softlayer_client, virtual_disk_image) } + end + end + + ## + # Retrieve the virtual servers (aka. CCIs or Virtual_Guests) associated with the # account. Unless you force these to update, they will be refreshed every # five minutes. # :call-seq: @@ -114,7 +252,45 @@ class Account < SoftLayer::ModelBase 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 + + ## + # 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. + # + # DEPRECATION WARNING: This method is deprecated in favor of find_vlan_with_number + # and will be removed in the next major release. + 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,9 +301,9 @@ 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) + Account.new(softlayer_client, network_hash) end ## @@ -137,4 +313,4 @@ def servers return self.bare_metal_servers + self.virtual_servers end end -end \ No newline at end of file +end diff --git a/lib/softlayer/AccountPassword.rb b/lib/softlayer/AccountPassword.rb new file mode 100644 index 0000000..874fd3f --- /dev/null +++ b/lib/softlayer/AccountPassword.rb @@ -0,0 +1,356 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer AccountPassword instance provides information about + # a user's password associated with a SoftLayer Account instance. + # + # This class roughly corresponds to the entity SoftLayer_Account_Password + # in the API. + # + class AccountPassword < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: + # A simple description of a username/password combination. + sl_attr :notes + + ## + # :attr_reader: + # The password portion of a username/password combination. + sl_attr :password + + ## + # :attr_reader: + # The username portion of a username/password combination. + sl_attr :username + + ## + # A description of the use for the account username/password combination. + # :call-seq: + # description(force_update=false) + sl_dynamic_attr :description do |resource| + resource.should_update? do + #only retrieved once per instance + @description == nil + end + + resource.to_update do + type = self.service.getType + type['description'] + end + end + + ## + # Updates the notes for the current account password. + # + def notes=(notes) + self.service.editObject({ "notes" => notes.to_s }) + self.refresh_details() + end + + ## + # Updates the password for the current account password. + # + def password=(password) + raise ArgumentError, "The new password cannot be nil" unless password + raise ArgumentError, "The new password cannot be empty" if password.empty? + + self.service.editObject({ "password" => password.to_s }) + self.refresh_details() + end + + ## + # Retrieve a list of network storage account passwords from all network storage devices. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include network storage account passwords from servers matching this datacenter + # * +:domain+ (string/array) - Include network storage account passwords from servers matching this domain + # * +:hostname+ (string/array) - Include network storage account passwords from servers matching this hostname + # * +:network_storage_server_type+ (symbol) - Include network storage account passwords attached to this server type + # * +:network_storage_type+ (symbol) - Include network storage account passwords from devices of this storage type + # * +:tags+ (string/array) - Include network storage account passwords from servers matching these tags + # * +:username+ (string/array) - Include network storage account passwords with this username only + # + # Additionally you may provide options related to the request itself: + # * *:account_password_object_filter* (ObjectFilter) - Include network storage account passwords for account passwords that match the + # criteria of this object filter + # * *:account_password_object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # * *:network_storage_object_filter* (ObjectFilter) - Include network storage account passwords from network storage that matches the + # criteria of this object filter + # + def self.find_network_storage_account_passwords(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? :network_storage_object_filter) + network_storage_object_filter = options_hash[:network_storage_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless network_storage_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + network_storage_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :account_password_object_filter) + account_password_object_filter = options_hash[:account_password_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless account_password_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + account_password_object_filter = ObjectFilter.new() + end + + if options_hash.has_key?(:network_storage_server_type) && ! [ :hardware, :virtual_server ].include?(options_hash[:network_storage_server_type]) + raise "Expected one of :hardware or :virtual_server for :network_storage_server_type option in #{__method__}" + end + + filter_label = { + :evault => "evaultNetworkStorage", + :hardware => "hardware", + :hub => "hubNetworkStorage", + :iscsi => "iscsiNetworkStorage", + :lockbox => "lockboxNetworkStorage", + :nas => "nasNetworkStorage", + :network_storage => "networkStorage", + :virtual_server => "virtualGuest" + } + + option_to_filter_path = { + :account_password => { + :username => "accountPassword.username" + }, + :network_storage => { + :datacenter => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.datacenter.name' ].join }, + :domain => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.domain' ].join }, + :hostname => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.hostname' ].join }, + :tags => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.tagReferences.tag.name' ].join } + } + } + + if options_hash[:network_storage_type] + unless filter_label.select{|label,filter| filter.end_with?("Storage")}.keys.include?(options_hash[:network_storage_type]) + raise "Expected :evault, :hub, :iscsi, :lockbox, :nas or :network_storage for option :network_storage_type in #{__method__}" + end + end + + if options_hash[:network_storage_server_type] + network_storage_type = options_hash[:network_storage_type] || :network_storage + + option_to_filter_path[:network_storage].keys.each do |option| + if options_hash[option] + network_storage_object_filter.modify do |filter| + filter.accept(option_to_filter_path[:network_storage][option].call(network_storage_type, options_hash[:network_storage_server_type])).when_it is(options_hash[option]) + end + end + end + end + + option_to_filter_path[:account_password].each do |option, filter_path| + account_password_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(network_storage_object_filter) unless network_storage_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + case options_hash[:network_storage_type] + when :evault + network_storage_data = account_service.getEvaultNetworkStorage + when :hub + network_storage_data = account_service.getHubNetworkStorage + when :iscsi + network_storage_data = account_service.getIscsiNetworkStorage + when :lockbox + network_storage_data = account_service.getLockboxNetworkStorage + when :nas + network_storage_data = account_service.getNasNetworkStorage + when :network_storage, nil + network_storage_data = account_service.getNetworkStorage + end + + account_passwords = network_storage_data.collect do |network_storage| + network_storage_service = softlayer_client[:Network_Storage].object_with_id(network_storage['id']) + network_storage_service = network_storage_service.object_filter(account_password_object_filter) unless account_password_object_filter.empty? + network_storage_service = network_storage_service.object_mask(AccountPassword.default_object_mask) + network_storage_service = network_storage_service.object_mask(options_hash[:account_password_object_mask]) if options_hash[:account_password_object_mask] + + account_password_data = network_storage_service.getAccountPassword + AccountPassword.new(softlayer_client, account_password_data) unless account_password_data.empty? + end + + account_passwords.compact + end + + ## + # Retrieve a list of network storage webcc passwords from all network storage devices. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include network storage webcc passwords from servers matching this datacenter + # * +:domain+ (string/array) - Include network storage webcc passwords from servers matching this domain + # * +:hostname+ (string/array) - Include network storage webcc passwords from servers matching this hostname + # * +:network_storage_server_type+ (symbol) - Include network storage webcc passwords attached to this server type + # * +:network_storage_type+ (symbol) - Include network storage webcc passwords from devices of this storage type + # * +:tags+ (string/array) - Include network storage webcc passwords from servers matching these tags + # * +:username+ (string/array) - Include network storage webcc passwords with this username only + # + # Additionally you may provide options related to the request itself: + # * *:network_storage_object_filter* (ObjectFilter) - Include network storage account passwords from network storage that matches the + # criteria of this object filter + # * *:webcc_password_object_filter* (ObjectFilter) - Include network storage account passwords for webcc passwords that match the + # criteria of this object filter + # * *:webcc_password_object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # + def self.find_network_storage_webcc_passwords(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? :network_storage_object_filter) + network_storage_object_filter = options_hash[:network_storage_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless network_storage_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + network_storage_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :webcc_password_object_filter) + webcc_password_object_filter = options_hash[:webcc_password_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless webcc_password_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + webcc_password_object_filter = ObjectFilter.new() + end + + if options_hash.has_key?(:network_storage_server_type) && ! [ :hardware, :virtual_server ].include?(options_hash[:network_storage_server_type]) + raise "Expected one of :hardware or :virtual_server for :network_storage_server_type option in #{__method__}" + end + + filter_label = { + :evault => "evaultNetworkStorage", + :hardware => "hardware", + :hub => "hubNetworkStorage", + :iscsi => "iscsiNetworkStorage", + :lockbox => "lockboxNetworkStorage", + :nas => "nasNetworkStorage", + :network_storage => "networkStorage", + :virtual_server => "virtualGuest" + } + + option_to_filter_path = { + :network_storage => { + :datacenter => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.datacenter.name' ].join }, + :domain => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.domain' ].join }, + :hostname => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.hostname' ].join }, + :tags => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.tagReferences.tag.name' ].join } + }, + :webcc_password => { + :username => "webccAccount.username" + } + } + + if options_hash[:network_storage_type] + unless filter_label.select{|label,filter| filter.end_with?("Storage")}.keys.include?(options_hash[:network_storage_type]) + raise "Expected :evault, :hub, :iscsi, :lockbox, :nas or :network_storage for option :network_storage_type in #{__method__}" + end + end + + if options_hash[:network_storage_server_type] + network_storage_type = options_hash[:network_storage_type] || :network_storage + + option_to_filter_path[:network_storage].keys.each do |option| + if options_hash[option] + network_storage_object_filter.modify do |filter| + filter.accept(option_to_filter_path[:network_storage][option].call(network_storage_type, options_hash[:network_storage_server_type])).when_it is(options_hash[option]) + end + end + end + end + + option_to_filter_path[:webcc_password].each do |option, filter_path| + webcc_password_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(network_storage_object_filter) unless network_storage_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + case options_hash[:network_storage_type] + when :evault + network_storage_data = account_service.getEvaultNetworkStorage + when :hub + network_storage_data = account_service.getHubNetworkStorage + when :iscsi + network_storage_data = account_service.getIscsiNetworkStorage + when :lockbox + network_storage_data = account_service.getLockboxNetworkStorage + when :nas + network_storage_data = account_service.getNasNetworkStorage + when :network_storage, nil + network_storage_data = account_service.getNetworkStorage + end + + webcc_passwords = network_storage_data.collect do |network_storage| + network_storage_service = softlayer_client[:Network_Storage].object_with_id(network_storage['id']) + network_storage_service = network_storage_service.object_filter(webcc_password_object_filter) unless webcc_password_object_filter.empty? + network_storage_service = network_storage_service.object_mask(AccountPassword.default_object_mask) + network_storage_service = network_storage_service.object_mask(options_hash[:webcc_password_object_mask]) if options_hash[:webcc_password_object_mask] + + webcc_password_data = network_storage_service.getWebccAccount + AccountPassword.new(softlayer_client, webcc_password_data) unless webcc_password_data.empty? + end + + webcc_passwords.compact + end + + ## + # Returns the service for interacting with this account password through the network API + # + def service + softlayer_client[:Account_Password].object_with_id(self.id) + end + + ## + # Make an API request to SoftLayer and return the latest properties hash + # for this object. + # + def softlayer_properties(object_mask = nil) + my_service = self.service + + if(object_mask) + my_service = my_service.object_mask(object_mask) + else + my_service = my_service.object_mask(self.class.default_object_mask) + end + + my_service.getObject() + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Account_Password)" => [ + 'id', + 'notes', + 'password', + 'username' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/BareMetalServer.rb b/lib/softlayer/BareMetalServer.rb index ebe36e0..f08f412 100644 --- a/lib/softlayer/BareMetalServer.rb +++ b/lib/softlayer/BareMetalServer.rb @@ -1,32 +1,19 @@ -# +#-- # 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 class represents a Bare Metal Server, a hardware server in contrast to a virtual machine, - # in the SoftLayer Environment. It corresponds rougly to the +SoftLayer_Hardware+ and + # in the SoftLayer Environment. It corresponds roughly to the +SoftLayer_Hardware+ and # +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 @@ -36,11 +23,11 @@ class BareMetalServer < Server # a Bare Metal Instance is physical, hardware server that is is provisioned to # match a profile with characteristics similar to a Virtual Server # - # This is an important distincition in rare cases, like cancelling the server. + # This is an important distinction in rare cases, like cancelling the 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,27 @@ 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.service.object_mask("mask[id]").getBillingItem['id'].to_i).cancelService() end end ## - # Returns the SoftLayer Service used to work with this Server + # Returns the username/password combinations for remote management accounts + # + def remote_management_accounts + self['remoteManagementAccounts'] + end + + ## + # 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 through 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 ## @@ -81,17 +76,18 @@ def service def self.default_object_mask sub_mask = { "mask(SoftLayer_Hardware_Server)" => [ - 'bareMetalInstanceFlag', - 'provisionDate', - 'hardwareStatus', - 'memoryCapacity', - 'processorPhysicalCoreAmount', - 'networkManagementIpAddress', - 'networkComponents[id, status, speed, maxSpeed, name, ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress, port, primarySubnet]', - 'activeTransaction[id, transactionStatus[friendlyName,name]]', - 'hardwareChassis[id, name]' - ] - } + 'activeTransaction[id, transactionStatus[friendlyName,name]]', + 'bareMetalInstanceFlag', + 'hardwareChassis[id, name]', + 'hardwareStatus', + 'memoryCapacity', + 'networkComponents[id, maxSpeed, name, ipmiIpAddress, ipmiMacAddress, macAddress, port, primaryIpAddress, primarySubnet, speed, status]', + 'networkManagementIpAddress', + 'processorPhysicalCoreAmount', + 'provisionDate', + 'remoteManagementAccounts[password,username]' + ] + } super.merge(sub_mask) end @@ -120,7 +116,37 @@ def self.cancellation_reasons end ## - # Retrive the bare metal server with the given server ID from the + # 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 + + ## + # Retrieve the bare metal server with the given server ID from the # SoftLayer API # # The options parameter should contain: @@ -129,11 +155,15 @@ def self.cancellation_reasons # # 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. + # + # Additionally you may provide options related to the request itself: + # * *:object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask 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) @@ -157,20 +187,22 @@ def self.server_with_id(server_id, options = {}) # # You may filter the list returned by adding options: # - # * +:tags+ (array) - an array of strings representing tags to search for on the instances - # * +:cpus+ (int) - return servers with the given number of (virtual) CPUs - # * +:memory+ (int) - return servers with at least the given amount of memory (in Gigabytes) - # * +:hostname+ (string) - return servers whose hostnames match the query string given (see ObjectFilter::query_to_filter_operation) - # * +:domain+ (string) - filter servers to those whose domain matches the query string given (see ObjectFilter::query_to_filter_operation) - # * +:datacenter+ (string) - find servers whose data center name matches the query string given (see ObjectFilter::query_to_filter_operation) - # * +:nic_speed+ (int) - include servers with the given nic speed (in Mbps) - # * +:public_ip+ (string) - return servers whose public IP address matches the query string given (see ObjectFilter::query_to_filter_operation) - # * +:private_ip+ (string) - same as :public_ip, but for private IP addresses + # * +:tags+ (string/array) - an array of strings representing tags to search for on the instances + # * +:cpus+ (int/array) - return servers with the given number of (virtual) CPUs + # * +:memory+ (int/array) - return servers with at least the given amount of memory (in Gigabytes) + # * +:hostname+ (string/array) - return servers whose hostnames match the query string given (see ObjectFilter::query_to_filter_operation) + # * +:domain+ (string/array) - filter servers to those whose domain matches the query string given (see ObjectFilter::query_to_filter_operation) + # * +:datacenter+ (string/array) - find servers whose data center name matches the query string given (see ObjectFilter::query_to_filter_operation) + # * +:nic_speed+ (int/array) - include servers with the given nic speed (in Mbps) + # * +:public_ip+ (string/array) - return servers whose public IP address matches the query string given (see ObjectFilter::query_to_filter_operation) + # * +:private_ip+ (string/array) - same as :public_ip, but for private IP addresses # # Additionally you may provide options related to the request itself: - # - # * +:object_mask+ (string, hash, or array) - The object mask of properties you wish to receive for the items returned If not provided, the result will use the default object mask - # * +:result_limit+ (hash with :limit, and :offset keys) - Limit the scope of results returned. + # * *:object_filter* (ObjectFilter) - Include hardware servers for servers that matche the + # criteria of this object filter + # * +:object_mask+ (string, hash, or array) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # * +:result_limit+ (hash with :limit, and :offset keys) - Limit the scope of results returned. # def self.find_servers(options_hash = {}) softlayer_client = options_hash[:client] || Client.default_client @@ -178,56 +210,41 @@ 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 = { - :cpus => "hardware.processorPhysicalCoreAmount", - :memory => "hardware.memoryCapacity", - :hostname => "hardware.hostname", - :domain => "hardware.domain", + :cpus => "hardware.processorPhysicalCoreAmount", + :memory => "hardware.memoryCapacity", + :hostname => "hardware.hostname", + :domain => "hardware.domain", :datacenter => "hardware.datacenter.name", - :nic_speed => "hardware.networkComponents.maxSpeed", - :public_ip => "hardware.primaryIpAddress", - :private_ip => "hardware.primaryBackendIpAddress" + :nic_speed => "hardware.networkComponents.maxSpeed", + :public_ip => "hardware.primaryIpAddress", + :private_ip => "hardware.primaryBackendIpAddress", + :tags => "hardware.tagReferences.tag.name" } # 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.merge!(SoftLayer::ObjectFilter.build(filter_path, options_hash[option])) if options_hash.has_key?(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", { - 'operation' => 'in', - 'options' => [{ - 'name' => 'data', - 'value' => options_hash[:tags] - }] - } )); + object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option])} if options_hash[option] 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) + account_service = account_service.object_mask(options_hash[:object_mask]) if options_hash[: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) + if options_hash[:result_limit] && options_hash[:result_limit][:offset] && options_hash[:result_limit][:limit] + account_service = account_service.result_limit(options_hash[:result_limit][:offset], options_hash[:result_limit][:limit]) end bare_metal_data = account_service.getHardware() bare_metal_data.collect { |server_data| BareMetalServer.new(softlayer_client, server_data) } end end #BareMetalServer -end \ No newline at end of file +end diff --git a/lib/softlayer/BareMetalServerOrder.rb b/lib/softlayer/BareMetalServerOrder.rb index 3e924fe..2749c9d 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 @@ -95,6 +78,14 @@ class BareMetalServerOrder # Object responding to to_s and providing a valid URI, The URI of a post provisioning script to run on # this server once it is created. # Corresponds to +postInstallScriptUri+ in the +createObject+ documentation + attr_accessor :provision_script_uri + + # Object responding to to_s and providing a valid URI, The URI of a post provisioning script to run on + # this server once it is created. + # Corresponds to +postInstallScriptUri+ in the +createObject+ documentation + # + # DEPRECATION WARNING: This attribute is deprecated in favor of provision_script_uri + # and will be removed in the next major release. attr_accessor :provision_script_URI # Boolean, If true then the server will only have a private network interface (and no public network interface) @@ -110,7 +101,7 @@ class BareMetalServerOrder attr_accessor :max_port_speed ## - # Create a new order that works thorugh the given client connection + # Create a new order that works through the given client connection def initialize (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 @@ -128,7 +119,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 +132,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) + return server_hash end protected @@ -164,15 +155,16 @@ 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['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['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['postInstallScriptUri'] = @provision_script_uri.to_s if @provision_script_uri + 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['sshKeys'] = @ssh_key_ids.collect { |ssh_key| {'id'=> ssh_key.to_i } } if @ssh_key_ids if @disks && !@disks.empty? template['hardDrives'] = @disks.collect do |disk| @@ -191,36 +183,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 + # Returns a list of the valid :os_reference_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 6f323f0..1d06a87 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). @@ -57,29 +42,57 @@ class BareMetalServerOrder_Package < Server # The domain of the server being created (i.e. 'softlayer.com' is the domain of sldn.softlayer.com) attr_accessor :domain - # The value of this property should be a hash. The keys of the hash are ProdcutItemCategory + # The value of this property should be a hash. The keys of the hash are ProductItemCategory # codes (like 'os' and 'ram') while the values may be Integers or Objects. The Integer values # should be the +id+ of a +SoftLayer_Product_Item_Price+ representing the configuration option # chosen for that category. Objects must respond to the +price_id+ message and return an integer # that is the +id+ of a +SoftLayer_Product_Item_Price+. Instances of the ProductConfigurationOption # class behave this way. # - # At a minimum, the configuation_options should include entries for each of the categories + # At a minimum, the configuration_options should include entries for each of the categories # required by the package (i.e. those returned from ProductPackage#required_categories) attr_accessor :configuration_options # The following properties are optional, but allow further fine tuning of # the 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() - attr_accessor :ssh_key_ids + # Boolean, If true, an hourly server will be ordered, otherwise a monthly server will be ordered + # Corresponds to +useHourlyPricing+ in the SoftLayer_Container_Product_Order_Hardware_Server container + # documentation + attr_accessor :hourly + + # An instance of the SoftLayer::ImageTemplate class. Represents the image template that should + # be installed on the server. + attr_accessor :image_template + + # Integer, The id of the public VLAN this server should join + # Corresponds to +primaryNetworkComponent.networkVlan.id+ in the +createObject+ documentation + attr_accessor :public_vlan_id + + # Integer, The id of the private VLAN this server should join + # Corresponds to +primaryBackendNetworkComponent.networkVlan.id+ in the +createObject+ documentation + attr_accessor :private_vlan_id + + # The URI of a script to execute on the server after it has been provisioned. This may be + # any object which accepts the to_s message. The resulting string will be passed to SoftLayer API. + attr_accessor :provision_script_uri # The URI of a script to execute on the server after it has been provisioned. This may be # any object which accepts the to_s message. The resulting string will be passed to SoftLayer API. + # + # DEPRECATION WARNING: This attribute is deprecated in favor of provision_script_uri + # and will be removed in the next major release. attr_accessor :provision_script_URI + # 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() + attr_accessor :ssh_key_ids + + # String, User metadata associated with the instance + # Corresponds to +userData+ in the +SoftLayer_Hardware_Server+ documentation + attr_accessor :user_metadata + ## # You initialize a BareMetalServerOrder_Package by passing in the package that you # are ordering from. @@ -103,7 +116,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 +134,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 @@ -131,18 +144,24 @@ def place_order! # based on the configuration options given. def hardware_order product_order = { - 'packageId' => @package.id, - 'useHourlyPricing' => false, - 'hardware' => { - 'hostname' => @hostname, - 'domain' => @domain - } + 'packageId' => @package.id, + 'hardware' => [{ + 'domain' => @domain, + 'hostname' => @hostname + }], + 'useHourlyPricing' => !!@hourly } - product_order['location'] = @package.location_id_for_datacenter_name(@datacenter.downcase) 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 + #Note that the use of image_template and SoftLayer::ProductPackage os/guest_diskX configuration category + #item prices is mutually exclusive. + product_order['hardware'][0]['userData'] = @user_metadata if @user_metadata + product_order['imageTemplateGlobalIdentifier'] = @image_template.global_id if @image_template + product_order['location'] = @datacenter.id if @datacenter + product_order['provisionScripts'] = [@provision_script_URI.to_s] if @provision_script_URI + product_order['provisionScripts'] = [@provision_script_uri.to_s] if @provision_script_uri + product_order['sshKeys'] = [{ 'sshKeyIds' => @ssh_key_ids }] if @ssh_key_ids + product_order['primaryNetworkComponent'] = { "networkVlan" => { "id" => @public_vlan_id.to_i } } if @public_vlan_id + product_order['primaryBackendNetworkComponent'] = { "networkVlan" => {"id" => @private_vlan_id.to_i } } if @private_vlan_id product_order['prices'] = @configuration_options.collect do |key, value| if value.respond_to?(:price_id) @@ -156,7 +175,5 @@ def hardware_order product_order end - end # BareMetalServerOrder_Package - -end # SoftLayer \ No newline at end of file +end # SoftLayer diff --git a/lib/softlayer/Client.rb b/lib/softlayer/Client.rb index 20bbd6c..d22ba32 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 central repository for the Service instances that call into the + # network API. # - # The following symbols can be used as hash arguments to pass options to the constructor: + # When you create a client, you pass in hash arguments specifying how the client + # should access the SoftLayer API. + # + # 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 @@ -34,23 +21,35 @@ module SoftLayer # class to provide the missing information. Please see that class for details. # class Client - # A username passed as authentication for each request. Cannot be emtpy or nil. + # A username passed as authentication for each request. Cannot be empty or nil. attr_reader :username - # An API key passed as part of the authentication of each request. Cannot be emtpy or nil. + # An API key passed as part of the authentication of each request. Cannot be empty or nil. attr_reader :api_key - # The base URL for requests that are passed to the server. Cannot be emtpy or nil. + # The base URL for requests that are passed to the server. Cannot be empty or nil. attr_reader :endpoint_url - # A string passsed as the value for the User-Agent header when requests are sent to SoftLayer API. + # A string passed 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) + attr_reader :network_timeout + ## # The client class maintains an (optional) default client. The default 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 @@ -59,13 +58,40 @@ def self.default_client=(new_default) @@default_client = new_default end + ## + # This will be using your username and password to get a portal + # token with which to authenticate client calls. + # This is a wrapper around Client.new. You can pass it the same + # parameters as with Client.new, with the exception that this will + # be expecting a password in the options hash. + def self.with_password(options = {}) + if options[:username].nil? || options[:username].empty? + raise 'A username is required to create this client' + end + + if options[:password].nil? || options[:password].empty? + raise 'A password is required to create this client' + end + + service = SoftLayer::Service.new('SoftLayer_User_Customer') + token = service.getPortalLoginToken( + options[:username], options[:password] + ) + + options[:userId] = token['userId'] + options[:authToken] = token['hash'] + + SoftLayer::Client.new(options) + end + ## # # Clients are built with a number of settings: # * +:username+ - The username of the account you wish to access through the API # * +:api_key+ - The API key used to authenticate the user with the API - # * +:enpoint_url+ - The API endpoint the client should connect to. This defaults to API_PUBLIC_ENDPOINT + # * +:endpoint_url+ - The API endpoint the client should connect to. This defaults to API_PUBLIC_ENDPOINT # * +:user_agent+ - A string that is passed along as the user agent when the client sends requests to the server + # * +:timeout+ - An integer number of seconds to wait until network requests time out. Corresponds to the network_timeout property of the client # # If these arguments are not provided then the client will try to locate them using other # sources including global variables, and the SoftLayer config file (if one exists) @@ -76,29 +102,57 @@ def initialize(options = {}) settings = Config.client_settings(options) # pick up the username from the options, the global, or assume no username - @username = settings[:username] || "" + @username = settings[:username] # do a similar thing for the api key - @api_key = settings[:api_key] || "" + @api_key = settings[:api_key] + + # grab token pair + @userId = settings[:userId] + @authToken = settings[:authToken] # 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})" - raise "A SoftLayer Client requires a username" if !@username || @username.empty? - raise "A SoftLayer Client requires an api_key" if !@api_key || @api_key.empty? - raise "A SoftLayer Clietn requires an enpoint URL" if !@endpoint_url || @endpoint_url.empty? + # and assign a time out if the settings offer one + @network_timeout = settings[:timeout] if settings.has_key?(:timeout) + + raise "A SoftLayer Client requires an endpoint URL" if !@endpoint_url || @endpoint_url.empty? + end + + # return whether this client is using token-based authentication + def token_based? + @userId && @authToken && !@authToken.empty? + end + + # return whether this client is using api_key-based authentication + def key_based? + @username && !@username.empty? && @api_key && !@api_key.empty? end # return a hash of the authentication headers for the client def authentication_headers - { - "authenticate" => { - "username" => @username, - "apiKey" => @api_key + if token_based? + { + 'authenticate' => { + 'complexType' => 'PortalLoginToken', + 'userId' => @userId, + 'authToken' => @authToken + } + } + elsif key_based? + { + 'authenticate' => { + 'username' => @username, + 'apiKey' => @api_key + } } - } + else + {} + end end # Returns a service with the given name. diff --git a/lib/softlayer/Config.rb b/lib/softlayer/Config.rb index a3404b4..87b9913 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. - # + # is collected in a hash with the keys `:username`, `:api_key`, and `:endpoint_url` respectively. + # # 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: @@ -52,13 +36,16 @@ module SoftLayer # SoftLayer-Python language bindings). A simple config file looks something like this: # # [softlayer] - # username = joeusername # api_key = DEADBEEFBADF00D + # endpoint_url = 'API_PUBLIC_ENDPOINT' + # timeout = 60 + # user_agent = "softlayer-ruby x.x.x" + # username = joeusername # # = Environment Variables # - # 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 + # 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 through # environment variables. # # = Global Variables @@ -67,7 +54,16 @@ module SoftLayer # # - +$SL_API_USERNAME+ # - +$SL_API_KEY+ - # - +$SL_API_BASE_URL+ + # - +$SL_API_BASE_URL+ (or alias +$SL_API_ENDPOINT_URL+) + # + # = XML RPC Variables + # + # The config allows for two variables that are passed on to the underlying XML RPC agent + # for interacting with the SoftLayer API (as with other settings these can be loaded from + # config file, environment variables, globals or provided values): + # + # - +SL_API_TIMEOUT+ + # - +SL_API_USER_AGENT+ # # = Direct parameters # @@ -75,56 +71,108 @@ module SoftLayer # of the key information is provided in that hash, that information will override # any discovered through the techniques above. # + class Config + ENDPOINT_URL_ALIAS = [ 'API_PRIVATE_ENDPOINT', 'API_PUBLIC_ENDPOINT' ] + FILE_LOCATIONS = [ '/etc/softlayer.conf', '~/.softlayer', './.softlayer' ] - class Config - def Config.globals_settings - result = {} - result[:username] = $SL_API_USERNAME if $SL_API_USERNAME - result[:api_key] = $SL_API_KEY if $SL_API_KEY - result[:endpoint_url] = $SL_API_BASE_URL || API_PUBLIC_ENDPOINT - result - end - - 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 - end - - FILE_LOCATIONS = ['/etc/softlayer.conf', '~/.softlayer', './.softlayer'] - - def Config.file_settings(*additional_files) - result = {} - - search_path = FILE_LOCATIONS - search_path = search_path + additional_files if additional_files - search_path = search_path.map { |file_path| File.expand_path(file_path) } - - search_path.each do |file_path| - if File.readable? file_path - config = ConfigParser.new file_path - softlayer_section = config["softlayer"] - - if softlayer_section - result[:username] = softlayer_section['username'] if softlayer_section['username'] - result[:endpoint_url] = softlayer_section['endpoint_url'] if softlayer_section['endpoint_url'] - result[:api_key] = softlayer_section['api_key'] if softlayer_section['api_key'] - end - end - end - - result - end - - def Config.client_settings(provided_settings = {}) + def Config.client_settings(provided_settings = {}) settings = { :endpoint_url => API_PUBLIC_ENDPOINT } - settings.merge! file_settings - settings.merge! environment_settings - settings.merge! globals_settings - settings.merge! provided_settings - - settings - end - end + + settings.merge! file_settings + settings.merge! environment_settings + settings.merge! globals_settings + settings.merge! provided_settings + + settings + end + + def Config.environment_settings + result = {} + + result[:api_key] = ENV['SL_API_KEY'] if ENV['SL_API_KEY'] + result[:user_agent] = ENV['SL_API_USER_AGENT'] || "softlayer_api gem/#{SoftLayer::VERSION} (Ruby #{RUBY_PLATFORM}/#{RUBY_VERSION})" + result[:username] = ENV['SL_USERNAME'] if ENV['SL_USERNAME'] + + if ENV['SL_API_BASE_URL'] && ENDPOINT_URL_ALIAS.include?(ENV['SL_API_BASE_URL']) + result[:endpoint_url] = (ENV["SL_API_BASE_URL"] == "API_PUBLIC_ENDPOINT" ? API_PUBLIC_ENDPOINT : API_PRIVATE_ENDPOINT) + elsif ENV['SL_API_ENDPOINT_URL'] && ENDPOINT_URL_ALIAS.include?(ENV['SL_API_ENDPOINT_URL']) + result[:endpoint_url] = (ENV["SL_API_ENDPOINT_URL"] == "API_PUBLIC_ENDPOINT" ? API_PUBLIC_ENDPOINT : API_PRIVATE_ENDPOINT) + elsif (ENV['SL_API_BASE_URL'] && ! ENDPOINT_URL_ALIAS.include?(ENV['SL_API_BASE_URL'])) || + (ENV['SL_API_ENDPOINT_URL'] && ! ENDPOINT_URL_ALIAS.include?(ENV['SL_API_ENDPOINT_URL'])) + result[:endpoint_url] = ENV['SL_API_BASE_URL'] || ENV['SL_API_ENDPOINT_URL'] + end + + begin + result[:timeout] = Integer(ENV['SL_API_TIMEOUT']) if ENV['SL_API_TIMEOUT'] + rescue => integer_parse_exception + raise "Expected the value of the timeout configuration property, '#{ENV['SL_API_TIMEOUT']}', to be parseable as an integer" + end + + result + end + + def Config.file_settings(*additional_files) + result = {} + + search_path = FILE_LOCATIONS + search_path = search_path + additional_files if additional_files + search_path = search_path.map { |file_path| File.expand_path(file_path) } + + search_path.each do |file_path| + if File.readable? file_path + config = ConfigParser.new file_path + profile_name = ENV['SL_PROFILE'] || 'softlayer' + softlayer_section = config[profile_name] + + if softlayer_section + result[:api_key] = softlayer_section['api_key'] if softlayer_section['api_key'] + result[:user_agent] = softlayer_section['user_agent'] || "softlayer_api gem/#{SoftLayer::VERSION} (Ruby #{RUBY_PLATFORM}/#{RUBY_VERSION})" + result[:username] = softlayer_section['username'] if softlayer_section['username'] + + if softlayer_section['base_url'] && ENDPOINT_URL_ALIAS.include?(softlayer_section['base_url']) + result[:endpoint_url] = (softlayer_section['base_url'] == "API_PUBLIC_ENDPOINT" ? API_PUBLIC_ENDPOINT : API_PRIVATE_ENDPOINT) + elsif softlayer_section['endpoint_url'] && ENDPOINT_URL_ALIAS.include?(softlayer_section['endpoint_url']) + result[:endpoint_url] = (softlayer_section['endpoint_url'] == "API_PUBLIC_ENDPOINT" ? API_PUBLIC_ENDPOINT : API_PRIVATE_ENDPOINT) + elsif (softlayer_section['base_url'] && ! ENDPOINT_URL_ALIAS.include?(softlayer_section['base_url'])) || + (softlayer_section['endpoint_url'] && ! ENDPOINT_URL_ALIAS.include?(softlayer_section['endpoint_url'])) + result[:endpoint_url] = softlayer_section['base_url'] || softlayer_section['endpoint_url'] + end + + begin + result[:timeout] = Integer(softlayer_section['timeout']) if softlayer_section['timeout'] + rescue => integer_parse_exception + raise "Expected the value of the timeout configuration property, '#{softlayer_section['timeout']}', to be parseable as an integer" + end + end + end + end + + result + end + + def Config.globals_settings + result = {} + + result[:api_key] = $SL_API_KEY if $SL_API_KEY + result[:user_agent] = $SL_API_USER_AGENT || "softlayer_api gem/#{SoftLayer::VERSION} (Ruby #{RUBY_PLATFORM}/#{RUBY_VERSION})" + result[:username] = $SL_API_USERNAME if $SL_API_USERNAME + + if $SL_API_ENDPOINT_URL && ENDPOINT_URL_ALIAS.include?($SL_API_ENDPOINT_URL) + result[:endpoint_url] = ($SL_API_ENDPOINT_URL == "API_PUBLIC_ENDPOINT" ? API_PUBLIC_ENDPOINT : API_PRIVATE_ENDPOINT) + elsif $SL_API_BASE_URL && ENDPOINT_URL_ALIAS.include?($SL_API_BASE_URL) + result[:endpoint_url] = ($SL_API_BASE_URL == "API_PUBLIC_ENDPOINT" ? API_PUBLIC_ENDPOINT : API_PRIVATE_ENDPOINT) + elsif ($SL_API_BASE_URL && ! ENDPOINT_URL_ALIAS.include?($SL_API_BASE_URL)) || + ($SL_API_ENDPOINT_URL && ! ENDPOINT_URL_ALIAS.include?($SL_API_ENDPOINT_URL)) + result[:endpoint_url] = $SL_API_ENDPOINT_URL || $SL_API_BASE_URL + end + + begin + result[:timeout] = Integer($SL_API_TIMEOUT) if $SL_API_TIMEOUT + rescue => integer_parse_exception + raise "Expected the value of the timeout configuration property, '#{$SL_API_TIMEOUT}', to be parseable as an integer" + end + + result + end + end end diff --git a/lib/softlayer/Datacenter.rb b/lib/softlayer/Datacenter.rb new file mode 100644 index 0000000..3ae4267 --- /dev/null +++ b/lib/softlayer/Datacenter.rb @@ -0,0 +1,61 @@ +#-- +# 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 + + ## + # :attr_reader: + # A short location description + sl_attr :name + + ## + # :attr_reader: long_name + # A longer location description + 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::default_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 diff --git a/lib/softlayer/DynamicAttribute.rb b/lib/softlayer/DynamicAttribute.rb index 788b2f1..b8785f9 100644 --- a/lib/softlayer/DynamicAttribute.rb +++ b/lib/softlayer/DynamicAttribute.rb @@ -1,29 +1,13 @@ -# +#-- # 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 module is inteneded to be used by classes in the SoftLayer + # This module is intended to be used by classes in the SoftLayer # object model. It creates a small DSL for creating attributes # that update themselves dynamically (usually by making requests # to the SoftLayer API) diff --git a/lib/softlayer/ImageTemplate.rb b/lib/softlayer/ImageTemplate.rb new file mode 100644 index 0000000..ea36d76 --- /dev/null +++ b/lib/softlayer/ImageTemplate.rb @@ -0,0 +1,381 @@ +#-- +# 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 roughly 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: notes + # The notes, if any, that are attached to the template. Can be nil. + sl_attr :notes, "note" + + ## + # :attr_reader: global_id + # 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 + Array(self['datacenters']).collect do |datacenter_data| + SoftLayer::Datacenter.datacenter_named(datacenter_data['name']) + end + 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 + # within 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 network 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/array) - Return templates with the given name + # * +:global_id+ (string/array) - Return templates with the given global identifier + # * +:tags+ (string/array) - Return templates with the tags + # + # Additionally you may provide options related to the request itself: + # + # * *:object_filter* (ObjectFilter) - Include private image templates for templates that matche the + # criteria of this object filter + # * +:object_mask+ (string, hash, or array) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # * +:result_limit+ (hash with :limit, and :offset keys) - Limit the scope of results returned. + 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", + :tags => "privateBlockDeviceTemplateGroups.tagReferences.tag.name" + } + + # 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 + + 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) + account_service = account_service.object_mask(options_hash[:object_mask]) if options_hash[:object_mask] + + if options_hash[:result_limit] && options_hash[:result_limit][:offset] && options_hash[:result_limit][:limit] + account_service = account_service.result_limit(options_hash[:result_limit][:offset], options_hash[:result_limit][:limit]) + end + + templates_data = Array(account_service.getPrivateBlockDeviceTemplateGroups) + templates_data.collect { |template_data| ImageTemplate.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/array) - Return templates with the given name + # * +:global_id+ (string/array) - Return templates with the given global identifier + # * +:tags+ (string/array) - Return templates with the tags + # + # Additionally you may provide options related to the request itself: + # + # * *:object_filter* (ObjectFilter) - Include public image templates for templates that matche the + # criteria of this object filter + # * +:object_mask+ (string, hash, or array) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # * +:result_limit+ (hash with :limit, and :offset keys) - Limit the scope of results returned. + 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 => "name", + :global_id => "globalIdentifier", + :tags => "tagReferences.tag.name" + } + + # 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 + + 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) + template_service = template_service.object_mask(options_hash[:object_mask]) if options_hash[:object_mask] + + if options_hash[:result_limit] && options_hash[:result_limit][:offset] && options_hash[:result_limit][:limit] + template_service = template_service.result_limit(options_hash[:result_limit][:offset], options_hash[:result_limit][:limit]) + end + + # Ensure this is an array with Array(). SL won't return array if the result set is of just one + # (like when the user specifies a limit of 1) + templates_data = Array(template_service.getPublicImages) + templates_data.collect { |template_data| ImageTemplate.new(softlayer_client, template_data) } + end + + ## + # Retrieve 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 = service.object_mask(default_object_mask) + service = service.object_mask(options_hash[:object_mask]) if options_hash[:object_mask] + + template_data = service.getObject + ImageTemplate.new(softlayer_client, template_data) + end + + ## + # Retrieve the image template with the given global ID. The routine searches the public image template list first + # and the private image template list if no public image with the given id is found. If no template is found + # after searching both lists, then the function returns nil. + # + # Should either search return more than one result (meaning the system found more than one template with the same + # global_id), then the routine will throw an exception. + # + # 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)) + if templates.empty? then + templates = find_private_templates(options_hash.merge(:global_id => global_id)) + end + raise "ImageTemplate::template_with_global_id returned more than one template with the same global id. This should not happen" if templates != nil && templates.count > 1 + 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 diff --git a/lib/softlayer/ModelBase.rb b/lib/softlayer/ModelBase.rb index d4a499b..862c0c2 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. @@ -85,7 +69,7 @@ def refresh_details(object_mask = nil) ## # Returns the value of of the given property as stored in the # softlayer_hash. This gives you access to the low-level, raw - # properties that underly this model object. The need for this + # properties that underlie this model object. The need for this # is not uncommon, but using this method should still be done # with deliberation. def [](softlayer_property) @@ -123,7 +107,7 @@ def to_ary() ## # Subclasses should implement this method as part of enabling the - # refresh_details fuctionality The implementation should make a request + # refresh_details functionality The implementation should make a request # to the SoftLayer API and retrieve an up-to-date SoftLayer hash # representation of this object. That hash should be the return value # of this routine. @@ -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..f4d548e --- /dev/null +++ b/lib/softlayer/NetworkComponent.rb @@ -0,0 +1,36 @@ +#-- +# 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 + ## + # :attr_reader: max_speed + # A network component's maximum allowed speed, + sl_attr :max_speed, 'maxSpeed' + + ## + # :attr_reader: + # A network component's maximum allowed speed, + # + # DEPRECATION WARNING: This attribute is deprecated in favor of max_speed + # and will be removed in the next major release. + sl_attr :maxSpeed + + ## + # :attr_reader: + # A network component's short name. + sl_attr :name + + # :attr_reader: + # A network component's port number. + sl_attr :port + + ## + # :attr_reader: + # A network component's speed, measured in Mbit per second. + sl_attr :speed + end +end diff --git a/lib/softlayer/NetworkMessageDelivery.rb b/lib/softlayer/NetworkMessageDelivery.rb new file mode 100644 index 0000000..b6e0366 --- /dev/null +++ b/lib/softlayer/NetworkMessageDelivery.rb @@ -0,0 +1,150 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer NetworkMessageDelivery instance provides information about + # the username/password combination for a specific Network Message Delivery + # account. + # + # This class roughly corresponds to the entity SoftLayer_Network_Message_Delivery + # in the API. + # + class NetworkMessageDelivery < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: created_at + # The date this username/password pair was created. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # The date this username/password pair was created. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: modified_at + # The date of the last modification to this username/password pair. + sl_attr :modified_at, 'modifyDate' + + ## + # :attr_reader: modified + # The date of the last modification to this username/password pair. + # DEPRECATION WARNING: This attribute is deprecated in favor of modified_at + # and will be removed in the next major release. + sl_attr :modified, 'modifyDate' + + ## + # :attr_reader: + # The password part of the username/password pair. + sl_attr :password + + ## + # :attr_reader: + # The username part of the username/password pair. + sl_attr :username + + ## + # Retrieve the message delivery type description of a network message delivery account. + # :call-seq: + # description(force_update=false) + sl_dynamic_attr :description do |resource| + resource.should_update? do + #only retrieved once per instance + @description == nil + end + + resource.to_update do + type = self.service.getType + type['description'] + end + end + + ## + # Retrieve the message delivery type name of a network message delivery account. + # :call-seq: + # name(force_update=false) + sl_dynamic_attr :name do |resource| + resource.should_update? do + #only retrieved once per instance + @name == nil + end + + resource.to_update do + type = self.service.getType + type['name'] + end + end + + ## + # Retrieve the vendor name for a network message delivery account. + # :call-seq: + # vendor(force_update=false) + sl_dynamic_attr :vendor do |resource| + resource.should_update? do + #only retrieved once per instance + @vendor == nil + end + + resource.to_update do + vendor = self.service.getVendor + vendor['name'] + end + end + + ## + # Updates the password for the current account password. + # + def password=(password) + raise ArgumentError, "The new password cannot be nil" unless password + raise ArgumentError, "The new password cannot be empty" if password.empty? + + self.service.editObject({ "password" => password.to_s }) + self.refresh_details() + end + + ## + # Returns the service for interacting with the network message delivery instance + # through the network API + # + def service + softlayer_client[:Network_Message_Delivery].object_with_id(self.id) + end + + ## + # Make an API request to SoftLayer and return the latest properties hash + # for this object. + # + def softlayer_properties(object_mask = nil) + my_service = self.service + + if(object_mask) + my_service = my_service.object_mask(object_mask) + else + my_service = my_service.object_mask(self.class.default_object_mask) + end + + my_service.getObject() + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Network_Message_Delivery)" => [ + 'createDate', + 'id', + 'modifyDate', + 'password', + 'username' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/NetworkMonitor.rb b/lib/softlayer/NetworkMonitor.rb new file mode 100644 index 0000000..2c55a59 --- /dev/null +++ b/lib/softlayer/NetworkMonitor.rb @@ -0,0 +1,333 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # This struct represents the network monitor levels of a server. + # It is roughly equivalent to SoftLayer data type + # SoftLayer_Network_Monitor_Version1_Query_Host_Stratum + class NetworkMonitorLevels < Struct.new(:monitor_level, :response_level) + def initialize(monitor_levels_data) + self.monitor_level = monitor_levels_data['monitorLevel'] + self.response_level = monitor_levels_data['responseLevel'] + end + end + + ## + # This struct represents a network monitor query result that shows the last + # state of the network monitor + class NetworkMonitorQueryResult < Struct.new(:finished_at, :responded_in, :response_status) + ## + # This constant is a mapping of network monitor query result statuses to descriptions + STATUS_DESCRIPTIONS = { + 0 => "Down/Critical: Server is down and/or has passed the critical response threshold (extremely long ping response, abnormal behavior, etc.).", + 1 => "Warning - Server may be recovering from a previous down state, or may have taken too long to respond.", + 2 => "Up", + 3 => "Not used", + 4 => "Unknown - An unknown error has occurred. If the problem persists, contact support.", + 5 => "Unknown - An unknown error has occurred. If the problem persists, contact support." + } + + def initialize(query_result_data) + self.finished_at = query_result_data['finishTime'] + self.responded_in = query_result_data['responseTime'] + self.response_status = query_result_data['responseStatus'] + end + end + + ## + # This struct represents a network monitor query type used for creating + # new network monitors. + class NetworkMonitorQueryType < Struct.new(:argument_description, :description, :id, :monitor_level, :name) + def initialize(query_type_data) + self.argument_description = query_type_data['arugmentDescription'] + self.description = query_type_data['description'] + self.id = query_type_data['monitorLevel'] + self.name = query_type_data['name'] + end + end + + ## + # This struct represents a network monitor response type used for configuring + # network monitor responses when created. + class NetworkMonitorResponseType < Struct.new(:action_description, :id, :level) + def initialize(response_type_data) + self.action_description = response_type_data['actionDescription'] + self.id = response_type_data['id'] + self.level = response_type_data['level'] + end + end + + ## + # Each SoftLayer NetworkMonitor instance provides information about network + # monitors configured to check host ping or host ports of servers. + # + # This class roughly corresponds to the entity SofyLayer_Network_Monitor_Version1_Query_Host + # in the API. + # + class NetworkMonitor < ModelBase + include ::SoftLayer::DynamicAttribute + + @@available_query_types = nil + @@available_response_types = nil + + ## + # :attr_reader: argument_value + # The argument to be used for this monitor, if necessary. The lowest monitoring levels (like ping) + # ignore this setting, but higher levels like HTTP custom use it. + sl_attr :argument_value, 'arg1Value' + + ## + # :attr_reader: ip_address + # The IP address to be monitored. Must be attached to the server on this object. + sl_attr :ip_address, 'ipAddress' + + ## + # :attr_reader: + # The status of this monitoring instance. Anything other than "ON" means that the monitor has been disabled. + sl_attr :status + + ## + # :attr_reader: wait_cycles + # The number of 5-minute cycles to wait before the "responseAction" is taken. If set to 0, the response + # action will be taken immediately. + sl_attr :wait_cycles, 'waitCycles' + + ## + # The most recent result for this particular monitoring instance. + # :call-seq: + # last_query_result(force_update=false) + sl_dynamic_attr :last_query_result do |resource| + resource.should_update? do + #only retrieved once per instance + @last_query_result == nil + end + + resource.to_update do + NetworkMonitorQueryResult.new(self.service.object_mask("mask[finishTime,responseStatus,responseTime]").getLastResult) + end + end + + ## + # The type of monitoring query that is executed when this server is monitored. + # :call-seq: + # query_type(force_update=false) + sl_dynamic_attr :query_type do |resource| + resource.should_update? do + #only retrieved once per instance + @query_type == nil + end + + resource.to_update do + NetworkMonitorQueryType.new(self.service.getQueryType) + end + end + + ## + # The response action taken when a monitor fails. + # :call-seq: + # response_type(force_update=false) + sl_dynamic_attr :response_type do |resource| + resource.should_update? do + #only retrieved once per instance + @response_type == nil + end + + resource.to_update do + NetworkMonitorResponseType.new(self.service.getResponseAction) + end + end + + ## + # Add a network monitor for a host ping or port check to a server. + # + def self.add_network_monitor(server, ip_address, query_type, response_type, wait_cycles = 0, argument_value = nil, 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 + raise "#{__method__} requires a server to monitor but none was given" if !server || !server.kind_of?(Server) + raise "#{__method__} requires an IP address to monitor but none was given" if !ip_address || ip_address.empty? + raise "#{__method__} requires a query type for the monitor but none was given" if !query_type || !query_type.kind_of?(NetworkMonitorQueryType) + raise "#{__method__} requires a response type for the monitor but none was given" if !response_type || !response_type.kind_of?(NetworkMonitorResponseType) + + if available_query_types(:client => softlayer_client, :query_level => server.network_monitor_levels.monitor_level).select{ |query| query.id == query_type.id }.empty? + raise "#{__method__} requested monitor query level is not supported for this server" + end + + if available_response_types(:client => softlayer_client, :response_level => server.network_monitor_levels.response_level).select{ |response| response.id == response_type.id }.empty? + raise "#{__method__} requested monitor response level is not supported for this server" + end + + network_monitor_object_filter = ObjectFilter.new() + server_id_label = server.kind_of?(VirtualServer) ? 'guestId' : 'hardwareId' + + network_monitor_object_filter.modify { |filter| filter.accept('networkMonitors.arg1Value').when_it is(argument_value.to_s) } + network_monitor_object_filter.modify { |filter| filter.accept('networkMonitors.' + server_id_label).when_it is(server.id) } + network_monitor_object_filter.modify { |filter| filter.accept('networkMonitors.ipAddress').when_it is(ip_address.to_s) } + network_monitor_object_filter.modify { |filter| filter.accept('networkMonitors.queryTypeId').when_it is(query_type.id) } + network_monitor_object_filter.modify { |filter| filter.accept('networkMonitors.responseActionId').when_it is(response_type.id) } + network_monitor_object_filter.modify { |filter| filter.accept('networkMonitors.waitCycles').when_it is(wait_cycles) } + + if server.service.object_filter(network_monitor_object_filter).getNetworkMonitors.empty? + network_monitor = softlayer_client[:Network_Monitor_Version1_Query_Host].createObject({ + 'arg1Value' => argument_value.to_s, + server_id_label => server.id, + 'ipAddress' => ip_address.to_s, + 'queryTypeId' => query_type.id, + 'responseActionId' => response_type.id, + 'waitCycles' => wait_cycles + }) + + NetworkMonitor.new(softlayer_client, network_monitor) + end + end + + ## + # Add user customers to the list of users notified on monitor failure for the specified server. Accepts a list of UserCustomer + # instances or user customer usernames. + # + def self.add_network_monitor_notification_users(server, user_customers, 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 + raise "#{__method__} requires a server to monitor but none was given" if !server || !server.kind_of?(Server) + raise "#{__method__} requires a list user customers but none was given" if !user_customers || user_customers.empty? + + user_customers_data = user_customers.map do |user_customer| + raise "#{__method__} requires a user customer but none was given" if !user_customer || (!user_customer.class.method_defined?(:username) && user_customer.empty?) + + user_customer_data = user_customer.class.method_defined?(:username) ? user_customer : UserCustomer.user_customer_with_username(user_customer, softlayer_client) + + raise "#{__method__} user customer with username #{user_customer.inspect} not found" unless user_customer_data + + user_customer_data + end + + current_user_customers = server.notified_network_monitor_users.map { |notified_network_monitor_user| notified_network_monitor_user['id'] } + + user_customers_data.delete_if { |user_customer| current_user_customers.include?(user_customer['id']) } + + unless user_customers_data.empty? + notification_monitor_user_service = server.kind_of?(VirtualServer) ? :User_Customer_Notification_Virtual_Guest : :User_Customer_Notification_Hardware + server_id_label = server.kind_of?(VirtualServer) ? 'guestId' : 'hardwareId' + + user_customer_notifications = user_customers_data.map { |user_customer| { server_id_label => server.id, 'userId' => user_customer['id'] } } + + softlayer_client[notification_monitor_user_service].createObjects(user_customer_notifications) + end + end + + ## + # Return the list of available query types (optionally limited to a max query level) + # + def self.available_query_types(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 + + unless @@available_query_types + available_query_types_data = softlayer_client[:Network_Monitor_Version1_Query_Host_Stratum].getAllQueryTypes + @@available_query_types = available_query_types_data.map{ |query_type| NetworkMonitorQueryType.new(query_type) } + end + + if options[:query_level] + @@available_query_types.select { |query_type| query_type.monitor_level.to_i <= options[:query_level].to_i } + else + @@available_query_types + end + end + + ## + # Return the list of available response types (optionally limited to a max response level) + # + def self.available_response_types(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 + + unless @@available_response_types + available_response_types_data = softlayer_client[:Network_Monitor_Version1_Query_Host_Stratum].getAllResponseTypes + @@available_response_types = available_response_types_data.map { |response_type| NetworkMonitorResponseType.new(response_type) } + end + + if options[:response_level] + @@available_response_types.select { |response_type| response_type.level.to_i <= options[:response_level].to_i } + else + @@available_response_types + end + end + + ## + # Rmove user customers from the list of users notified on monitor failure for the specified server. Accepts a list of UserCustomer + # instances or user customer usernames. + # + def self.remove_network_monitor_notification_users(server, user_customers, 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 + raise "#{__method__} requires a server to monitor but none was given" if !server || !server.kind_of?(Server) + raise "#{__method__} requires a list user customers but none was given" if !user_customers || user_customers.empty? + + user_customers_data = user_customers.map do |user_customer| + raise "#{__method__} requires a user customer but none was given" if !user_customer || (!user_customer.kind_of?(UserCustomer) && user_customer.empty?) + + user_customer_data = user_customer.kind_of?(UserCustomer) ? user_customer : UserCustomer.user_customer_with_username(user_customer, softlayer_client) + + raise "#{__method__} user customer with username #{user_customer.inspect} not found" unless user_customer_data + + user_customer_data + end + + current_user_customers = user_customers_data.map { |user_customer| user_customer['id'] } + monitor_user_notification_object_filter = ObjectFilter.new() + + monitor_user_notification_object_filter.modify { |filter| filter.accept('monitoringUserNotification.userId').when_it is(current_user_customers) } + + monitor_user_notification_data = server.service.object_filter(monitor_user_notification_object_filter).object_mask("mask[id]").getMonitoringUserNotification + + unless monitor_user_notification_data.empty? + notification_monitor_user_service = server.kind_of?(VirtualServer) ? :User_Customer_Notification_Virtual_Guest : :User_Customer_Notification_Hardware + + softlayer_client[notification_monitor_user_service].deleteObjects(monitor_user_notification_data) + end + end + + ## + # Removes the list of network monitors from their associated servers. Accpets a list of NetworkMonitor instances or id's. + # + def self.remove_network_monitors(network_monitors, 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 + + network_monitors_data = network_monitors.map do |network_monitor| + raise "#{__method__} requires a network monitor instance or id but non provided" if !network_monitor || (!network_monitor.kind_of?(NetworkMonitor) && network_monitor.empty?) + + network_monitor.kind_of?(NetworkMonitor) ? { 'id' => network_monitor['id'] } : { 'id' => network_monitor } + end + + softlayer_client[:Network_Monitor_Version1_Query_Host].deleteObjects(network_monitors_data) + end + + ## + # Returns the service for interacting with this network monitor component through the network API + # + def service + softlayer_client[:Network_Monitor_Version1_Query_Host].object_with_id(self.id) + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Network_Monitor_Version1_Query_Host)" => [ + 'arg1Value', + 'guestId', + 'hardwareId', + 'hostId', + 'id', + 'ipAddress', + 'status', + 'waitCycles' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/NetworkService.rb b/lib/softlayer/NetworkService.rb new file mode 100644 index 0000000..6c5038a --- /dev/null +++ b/lib/softlayer/NetworkService.rb @@ -0,0 +1,97 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer NetworkService instance provides connectivity + # information for a specific Network Service Resource. + # + # This class roughly corresponds to the entity SoftLayer_Network_Service_Resource + # in the API. + # + class NetworkService < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: + # The name associated with this resource + sl_attr :name + + ## + # :attr_reader: private_ip + # The backend IP address for this resource + sl_attr :private_ip, 'backendIpAddress' + + ## + # :attr_reader: public_ip + # The frontend IP address for this resource + sl_attr :public_ip, 'frontendIpAddress' + + ## + # :attr_reader: ssh_username + # The ssh username of for this resource + sl_attr :ssh_username, 'sshUsername' + + ## + # Retrieve the datacenter that this network service resource is available in + # :call-seq: + # datacenter(force_update=false) + sl_dynamic_attr :datacenter do |resource| + resource.should_update? do + #only retrieved once per instance + @datacenter == nil + end + + resource.to_update do + Datacenter::datacenter_named(self['datacenter']['name'], self.softlayer_client) + end + end + + ## + # Returns the api properties used to connect to the network service resource + # + def api + { + 'host' => self['apiHost'], + 'password' => self['apiPassword'], + 'path' => self['apiPath'], + 'port' => self['apiPort'], + 'protocol' => self['apiProtocol'], + 'username' => self['apiUsername'] + } + end + + ## + # Returns the network service resource type name + # + def type + self['type']['type'] + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Network_Service_Resource)" => [ + 'apiHost', + 'apiPassword', + 'apiPath', + 'apiPort', + 'apiProtocol', + 'apiUsername', + 'backendIpAddress', + 'datacenter', + 'frontendIpAddress', + 'id', + 'name', + 'networkDevice.id', + 'sshUsername', + 'type.type' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/NetworkStorage.rb b/lib/softlayer/NetworkStorage.rb new file mode 100644 index 0000000..a10eb9f --- /dev/null +++ b/lib/softlayer/NetworkStorage.rb @@ -0,0 +1,353 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer NetworkStorage instance provides information about + # a storage product and access credentials. + # + # This class roughly corresponds to the entity SoftLayer_Network_Storage + # in the API. + # + class NetworkStorage < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: capacity + # A Storage account's capacity, measured in gigabytes. + sl_attr :capacity, 'capacityGb' + + ## + # :attr_reader: created_at + # The date a network storage volume was created. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # The date a network storage volume was created. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: + # Public notes related to a Storage volume. + sl_attr :notes + + ## + # :attr_reader: + # The password used to access a non-EVault Storage volume. + # This password is used to register the EVault server agent with the + # vault backup system. + sl_attr :password + + ## + # :attr_reader: type + # A Storage account's type. + sl_attr :type, 'nasType' + + ## + # :attr_reader: upgradable + # This flag indicates whether this storage type is upgradable or not. + sl_attr :upgradable, 'upgradableFlag' + + ## + # :attr_reader: + # The username used to access a non-EVault Storage volume. + # This username is used to register the EVault server agent with the + # vault backup system. + sl_attr :username + + ## + # Retrieve other usernames and passwords associated with a Storage volume. + # :call-seq: + # account_password(force_update=false) + sl_dynamic_attr :account_password do |resource| + resource.should_update? do + #only retrieved once per instance + @account_password == nil + end + + resource.to_update do + account_password = self.service.object_mask(AccountPassword.default_object_mask).getAccountPassword + AccountPassword.new(softlayer_client, account_password) unless account_password.empty? + end + end + + ## + # A Storage volume's access credentials. + # :call-seq: + # credentials(force_update=false) + sl_dynamic_attr :credentials do |resource| + resource.should_update? do + #only retrieved once per instance + @credentials == nil + end + + resource.to_update do + self.service.object_mask(NetworkStorageCredential.default_object_mask).getCredentials.collect{|cred| NetworkStorageCredential.new(softlayer_client, cred) } + end + end + + ## + # The network resource a Storage service is connected to. + # :call-seq: + # service_resource(force_update=false) + sl_dynamic_attr :service_resource do |resource| + resource.should_update? do + #only retrieved once per instance + @service_resource == nil + end + + resource.to_update do + NetworkService.new(softlayer_client, self.service.object_mask(NetworkService.default_object_mask).getServiceResource) + end + end + + ## + # The account username and password for the EVault webCC interface. + sl_dynamic_attr :webcc_account do |resource| + resource.should_update? do + #only retrieved once per instance + @webcc_account == nil + end + + resource.to_update do + webcc_account = self.service.object_mask(AccountPassword.default_object_mask).getWebccAccount + AccountPassword.new(softlayer_client, webcc_account) unless webcc_account.empty? + end + end + + ## + # Add a username/password credential to the network storage instance + # + def add_credential(credential_type) + raise ArgumentError, "The new credential type cannot be nil" unless credential_type + raise ArgumentError, "The new credential type cannot be empty" if credential_type.empty? + + new_credential = self.service.object_mask(NetworkStorageCredential.default_object_mask).assignNewCredential(credential_type.to_s) + + @credentials = nil + + NetworkStorageCredential.new(softlayer_client, new_credential) unless new_credential.empty? + end + + ## + # Assign an existing network storage credential specified by the username to the network storage instance + # + def assign_credential(username) + raise ArgumentError, "The username cannot be nil" unless username + raise ArgumentError, "The username cannot be empty" if username.empty? + + self.service.assignCredential(username.to_s) + + @credentials = nil + end + + ## + # Determines if one of the credentials pertains to the specified username. + # + def has_user_credential?(username) + self.credentials.map { |credential| credential.username }.include?(username) + end + + ## + # Updates the notes for the network storage instance. + # + def notes=(notes) + self.service.editObject({ "notes" => notes.to_s }) + self.refresh_details() + end + + ## + # Updates the password for the network storage instance. + # + def password=(password) + raise ArgumentError, "The new password cannot be nil" unless password + raise ArgumentError, "The new password cannot be empty" if password.empty? + + self.service.editObject({ "password" => password.to_s }) + self.refresh_details() + end + + ## + # Remove an existing network storage credential specified by the username from the network storage instance + # + def remove_credential(username) + raise ArgumentError, "The username cannot be nil" unless username + raise ArgumentError, "The username cannot be empty" if username.empty? + + self.service.removeCredential(username.to_s) + + @credentials = nil + end + + ## + # Retrieve a list of network storage services. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include network storage associated with servers matching this datacenter + # * +:domain+ (string/array) - Include network storage associated with servers matching this domain + # * +:hostname+ (string/array) - Include network storage associated with servers matching this hostname + # * +:network_storage_server_type+ (symbol) - Include network storage associated with this server type + # * +:network_storage_type+ (symbol) - Include network storage from devices of this storage type + # * +:service+ (string/array) - Include network storage from devices with this service fqdn + # * +:tags+ (string/array) - Include network storage associated with servers matching these tags + # + # Additionally you may provide options related to the request itself: + # * *:network_storage_object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # * *:network_storage_object_filter* (ObjectFilter) - Include network storage credentials from network storage that matches the + # criteria of this object filter + # * +:result_limit+ (hash with :limit, and :offset keys) - Limit the scope of results returned. + # + def self.find_network_storage(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? :network_storage_object_filter) + network_storage_object_filter = options_hash[:network_storage_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless network_storage_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + network_storage_object_filter = ObjectFilter.new() + end + + if options_hash.has_key?(:network_storage_server_type) && ! [ :hardware, :virtual_server ].include?(options_hash[:network_storage_server_type]) + raise "Expected one of :hardware or :virtual_server for :network_storage_server_type option in #{__method__}" + end + + filter_label = { + :evault => "evaultNetworkStorage", + :hardware => "hardware", + :hub => "hubNetworkStorage", + :iscsi => "iscsiNetworkStorage", + :lockbox => "lockboxNetworkStorage", + :nas => "nasNetworkStorage", + :network_storage => "networkStorage", + :virtual_server => "virtualGuest" + } + + option_to_filter_path = { + :datacenter => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.datacenter.name' ].join }, + :domain => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.domain' ].join }, + :hostname => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.hostname' ].join }, + :service => lambda { |storage_type| return [ filter_label[storage_type], '.serviceResource.backendIpAddress' ].join }, + :tags => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.tagReferences.tag.name' ].join }, + } + + if options_hash[:network_storage_type] + unless filter_label.select{|label,filter| filter.end_with?("Storage")}.keys.include?(options_hash[:network_storage_type]) + raise "Expected :evault, :hub, :iscsi, :lockbox, :nas or :network_storage for option :network_storage_type in #{__method__}" + end + end + + network_storage_type = options_hash[:network_storage_type] || :network_storage + + if options_hash[:service] + network_storage_object_filter.modify do |filter| + filter.accept(option_to_filter_path[:service].call(network_storage_type)).when_it is(options_hash[:service]) + end + end + + if options_hash[:network_storage_server_type] + [ :datacenter, :domain, :hostname, :tags ].each do |option| + if options_hash[option] + network_storage_object_filter.modify do |filter| + filter.accept(option_to_filter_path[option].call(network_storage_type, options_hash[:network_storage_server_type])).when_it is(options_hash[option]) + end + end + end + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(network_storage_object_filter) unless network_storage_object_filter.empty? + account_service = account_service.object_mask(NetworkStorage.default_object_mask) + account_service = account_service.object_mask(options_hash[:network_storage_object_mask]) if options_hash[:network_storage_object_mask] + + if options_hash[:result_limit] && options_hash[:result_limit][:offset] && options_hash[:result_limit][:limit] + account_service = account_service.result_limit(options_hash[:result_limit][:offset], options_hash[:result_limit][:limit]) + end + + case options_hash[:network_storage_type] + when :evault + network_storage_data = account_service.getEvaultNetworkStorage + when :hub + network_storage_data = account_service.getHubNetworkStorage + when :iscsi + network_storage_data = account_service.getIscsiNetworkStorage + when :lockbox + network_storage_data = account_service.getLockboxNetworkStorage + when :nas + network_storage_data = account_service.getNasNetworkStorage + when :network_storage, nil + network_storage_data = account_service.getNetworkStorage + end + + network_storage_data.collect { |network_storage| NetworkStorage.new(softlayer_client, network_storage) unless network_storage.empty? }.compact + end + + ## + # Returns the service for interacting with this network storage through the network API + # + def service + softlayer_client[:Network_Storage].object_with_id(self.id) + end + + ## + # Make an API request to SoftLayer and return the latest properties hash + # for this object. + # + def softlayer_properties(object_mask = nil) + my_service = self.service + + if(object_mask) + my_service = my_service.object_mask(object_mask) + else + my_service = my_service.object_mask(self.class.default_object_mask) + end + + my_service.getObject() + end + + ## + # Updates the password for the network storage credential of the username specified. + # + def update_credential_password(username, password) + raise ArgumentError, "The new password cannot be nil" unless password + raise ArgumentError, "The new username cannot be nil" unless username + raise ArgumentError, "The new password cannot be empty" if password.empty? + raise ArgumentError, "The new username cannot be empty" if username.empty? + + self.service.editCredential(username.to_s, password.to_s) + + @credentials = nil + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Network_Storage)" => [ + 'capacityGb', + 'createDate', + 'id', + 'nasType', + 'notes', + 'password', + 'upgradableFlag', + 'username' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/NetworkStorageAllowedHost.rb b/lib/softlayer/NetworkStorageAllowedHost.rb new file mode 100644 index 0000000..15f7b7e --- /dev/null +++ b/lib/softlayer/NetworkStorageAllowedHost.rb @@ -0,0 +1,83 @@ +module SoftLayer + ## + # Each SoftLayer NetworkStorageAllowedHost instance provides information about + # a hosts allowed access to a storage product group. + # + # This class roughly corresponds to the entity SoftLayer_Network_Storage_Allowed_Host + # in the API. + # + class NetworkStorageAllowedHost < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: + # The name of allowed host, usually an IQN or other identifier + sl_attr :name + + ## + # Retrieve the NetworkStorageGroup instances assigned to this host + # :call-seq: + # assigned_groups(force_update=false) + sl_dynamic_attr :assigned_groups do |resource| + resource.should_update? do + #only retrieved once per instance + @assigned_groups == nil + end + + resource.to_update do + assigned_groups = self.service.object_mask(NetworkStorageGroup.default_object_mask).getAssignedGroups + assigned_groups.collect { |assigned_group| NetworkStorageGroup.new(softlayer_client, assigned_group) unless assigned_group.empty? }.compact + end + end + + ## + # Retrieve the NetworkStorage instances assigned to this host + # :call-seq: + # assigned_volumes(force_update=false) + sl_dynamic_attr :assigned_volumes do |resource| + resource.should_update? do + #only retrieved once per instance + @assigned_volumes == nil + end + + resource.to_update do + assigned_volumes = self.service.object_mask(NetworkStorage.default_object_mask).getAssignedVolumes + assigned_volumes.collect { |assigned_volume| NetworkStorage.new(softlayer_client, assigned_volume) unless assigned_volume.empty? }.compact + end + end + + ## + # Retrieve the NetworkStorageCredential instance used to access NetworkStorage for this host + # :call-seq: + # credential(force_update=false) + sl_dynamic_attr :credential do |resource| + resource.should_update? do + #only retrieved once per instance + @credential == nil + end + + resource.to_update do + credential = self.service.object_mask(NetworkStorageCredential.default_object_mask).getCredential + NetworkStorageCredential.new(softlayer_client, credential) unless credential.empty? + end + end + + ## + # Returns the service for interacting with this network storage through the network API + # + def service + softlayer_client[:Network_Storage_Allowed_Host].object_with_id(self.id) + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Network_Storage_Allowed_Host)" => [ + 'id', + 'name' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/NetworkStorageCredential.rb b/lib/softlayer/NetworkStorageCredential.rb new file mode 100644 index 0000000..748c39c --- /dev/null +++ b/lib/softlayer/NetworkStorageCredential.rb @@ -0,0 +1,219 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer NetworkStorageCredential instance provides information + # on a username/password credential combination used to access a specific + # Network Storage. + # + # This class roughly corresponds to the entity SoftLayer_Network_Storage_Credential + # in the API. + # + class NetworkStorageCredential < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: created_at + # This is the data that the record was created in the table. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # This is the data that the record was created in the table. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: modified_at + # This is the date that the record was last updated in the table. + sl_attr :modified_at, 'modifyDate' + + ## + # :attr_reader: modified + # This is the date that the record was last updated in the table. + # DEPRECATION WARNING: This attribute is deprecated in favor of modified_at + # and will be removed in the next major release. + sl_attr :modified, 'modifyDate' + + ## + # :attr_reader: + # This is the password associated with the volume. + sl_attr :password + + ## + # :attr_reader: + # This is the username associated with the volume. + sl_attr :username + + ## + # Returns a description of the Network Storage Credential type + # + def description + self['type']['description'] + end + + ## + # Returns the name of the Network Storage Credential type + # + def name + self['type']['name'] + end + + ## + # Retrieve a list of network storage credentials from all network storage devices. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include network storage credentials associated with servers matching this datacenter + # * +:domain+ (string/array) - Include network storage credentials associated with servers matching this domain + # * +:hostname+ (string/array) - Include network storage credentials associated with servers matching this hostname + # * +:network_storage_server_type+ (symbol) - Include network storage credentials associated with services of this server type + # * +:network_storage_type+ (symbol) - Include network storage credentials from devices of this storage type + # * +:service+ (string/array) - Include network storage credentials from devices with this service fqdn + # * +:tags+ (string/array) - Include network storage credentials associated with servers matching these tags + # * +:username+ (string/array) - Include network storage credentials with this username only + # + # Additionally you may provide options related to the request itself: + # * *:network_storage_credential_object_filter* (ObjectFilter) - Include network storage credentials that match the + # criteria of this object filter + # * *:network_storage_credential_object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # * *:network_storage_object_filter* (ObjectFilter) - Include network storage credentials from network storage that matches the + # criteria of this object filter + # + def self.find_network_storage_credentials(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? :network_storage_object_filter) + network_storage_object_filter = options_hash[:network_storage_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless network_storage_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + network_storage_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :network_storage_credential_object_filter) + network_storage_credential_object_filter = options_hash[:network_storage_credential_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless network_storage_credential_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + network_storage_credential_object_filter = ObjectFilter.new() + end + + if options_hash.has_key?(:network_storage_server_type) && ! [ :hardware, :virtual_server ].include?(options_hash[:network_storage_server_type]) + raise "Expected one of :hardware or :virtual_server for :network_storage_server_type option in #{__method__}" + end + + filter_label = { + :evault => "evaultNetworkStorage", + :hardware => "hardware", + :hub => "hubNetworkStorage", + :iscsi => "iscsiNetworkStorage", + :lockbox => "lockboxNetworkStorage", + :nas => "nasNetworkStorage", + :network_storage => "networkStorage", + :virtual_server => "virtualGuest" + } + + option_to_filter_path = { + :network_storage => { + :datacenter => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.datacenter.name' ].join }, + :domain => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.domain' ].join }, + :hostname => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.hostname' ].join }, + :service => lambda { |storage_type| return [ filter_label[storage_type], '.serviceResource.backendIpAddress' ].join }, + :tags => lambda { |storage_type, server_type| return [ filter_label[storage_type], '.', filter_label[server_type], '.tagReferences.tag.name' ].join } + }, + :network_storage_credential => { + :username => "credentials.username" + } + } + + if options_hash[:network_storage_type] + unless filter_label.select{|label,filter| filter.end_with?("Storage")}.keys.include?(options_hash[:network_storage_type]) + raise "Expected :evault, :hub, :iscsi, :lockbox, :nas or :network_storage for option :network_storage_type in #{__method__}" + end + end + + network_storage_type = options_hash[:network_storage_type] || :network_storage + + if options_hash[:service] + network_storage_object_filter.modify do |filter| + filter.accept(option_to_filter_path[:network_storage][:service].call(network_storage_type)).when_it is(options_hash[:service]) + end + end + + if options_hash[:network_storage_server_type] + option_to_filter_path[:network_storage].keys.each do |option| + next if option == :service + + if options_hash[option] + network_storage_object_filter.modify do |filter| + filter.accept(option_to_filter_path[:network_storage][option].call(network_storage_type, options_hash[:network_storage_server_type])).when_it is(options_hash[option]) + end + end + end + end + + option_to_filter_path[:network_storage_credential].each do |option, filter_path| + network_storage_credential_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(network_storage_object_filter) unless network_storage_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + case options_hash[:network_storage_type] + when :evault + network_storage_data = account_service.getEvaultNetworkStorage + when :hub + network_storage_data = account_service.getHubNetworkStorage + when :iscsi + network_storage_data = account_service.getIscsiNetworkStorage + when :lockbox + network_storage_data = account_service.getLockboxNetworkStorage + when :nas + network_storage_data = account_service.getNasNetworkStorage + when :network_storage, nil + network_storage_data = account_service.getNetworkStorage + end + + network_storage_credentials = network_storage_data.collect do |network_storage| + network_storage_service = softlayer_client[:Network_Storage].object_with_id(network_storage['id']) + network_storage_service = network_storage_service.object_filter(network_storage_credential_object_filter) unless network_storage_credential_object_filter.empty? + network_storage_service = network_storage_service.object_mask(NetworkStorageCredential.default_object_mask) + network_storage_service = network_storage_service.object_mask(options_hash[:network_storage_credential_object_mask]) if options_hash[:network_storage_credential_object_mask] + + network_storage_credentials_data = network_storage_service.getCredentials + network_storage_credentials_data.map { |credential| NetworkStorageCredential.new(softlayer_client, credential) unless credential.empty? }.compact + end + + network_storage_credentials.flatten + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Network_Storage_Credential)" => [ + 'createDate', + 'id', + 'modifyDate', + 'password', + 'type.description', + 'type.name', + 'username' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/NetworkStorageGroup.rb b/lib/softlayer/NetworkStorageGroup.rb new file mode 100644 index 0000000..8bf59bd --- /dev/null +++ b/lib/softlayer/NetworkStorageGroup.rb @@ -0,0 +1,191 @@ +module SoftLayer + ## + # Each SoftLayer NetworkStorageGroup instance provides information about + # a storage product group and hosts allowed access. + # + # This class roughly corresponds to the entity SoftLayer_Network_Storage_Group + # in the API. + # + class NetworkStorageGroup < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: + # The friendly name of this group + sl_attr :alias + + ## + # :attr_reader: created_at + # The date this group was created. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # The date this group was created. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: modified_at + # The date this group was modified. + sl_attr :modified_at, 'modifyDate' + + ## + # :attr_reader: modified + # The date this group was modified. + # DEPRECATION WARNING: This attribute is deprecated in favor of modified_at + # and will be removed in the next major release. + sl_attr :modified, 'modifyDate' + + ## + # Retrieve the SoftLayer_Account which owns this group. + # :call-seq: + # account(force_update=false) + sl_dynamic_attr :account do |resource| + resource.should_update? do + #only retrieved once per instance + @account == nil + end + + resource.to_update do + account = self.service.getAccount + Account.new(softlayer_client, account) unless account.empty? + end + end + + ## + # Retrieve the allowed hosts list for this group. + # :call-seq: + # allowed_hosts(force_update=false) + sl_dynamic_attr :allowed_hosts do |resource| + resource.should_update? do + #only retrieved once per instance + @allowed_hosts == nil + end + + resource.to_update do + allowed_hosts = self.service.object_mask(NetworkStorageAllowedHost.default_object_mask).getAllowedHosts + allowed_hosts.collect { |allowed_host| NetworkStorageAllowedHost.new(softlayer_client, allowed_host) unless allowed_host.empty? }.compact + end + end + + ## + # Retrieve the network storage volumes this group is attached to. + # :call-seq: + # attached_volumes(force_update=false) + sl_dynamic_attr :attached_volumes do |resource| + resource.should_update? do + #only retrieved once per instance + @attached_volumes == nil + end + + resource.to_update do + attached_volumes = self.service.object_mask(NetworkStorage.default_object_mask).getAttachedVolumes + attached_volumes.collect { |attached_volume| NetworkStorage.new(softlayer_client, attached_volume) unless attached_volume.empty? }.compact + end + end + + ## + # Retrieve the IP address for for SoftLayer_Network_Storage_Allowed_Host objects within this group. + # :call-seq: + # ip_address(force_update=false) + sl_dynamic_attr :ip_address do |resource| + resource.should_update? do + #only retrieved once per instance + @ip_address == nil + end + + resource.to_update do + network_connection_details = self.service.getNetworkConnectionDetails + network_connection_details["ipAddress"] unless network_connection_details.empty? + end + end + + ## + # Retrieve the description of the SoftLayer_Network_Storage_OS_Type + # Operating System designation that this group was created for. + # :call-seq: + # os_description(force_update=false) + sl_dynamic_attr :os_description do |resource| + resource.should_update? do + #only retrieved once per instance + @os_description == nil + end + + resource.to_update do + os_type = self.service.getOsType + os_type["description"] unless os_type.empty? + end + end + + ## + # Retrieve the name of the SoftLayer_Network_Storage_OS_Type + # Operating System designation that this group was created for. + # :call-seq: + # os_name(force_update=false) + sl_dynamic_attr :os_name do |resource| + resource.should_update? do + #only retrieved once per instance + @os_name == nil + end + + resource.to_update do + os_type = self.service.getOsType + os_type["name"] unless os_type.empty? + end + end + + ## + # Retrieve the network resource this group is created on. + # :call-seq: + # service_resource(force_update=false) + sl_dynamic_attr :service_resource do |resource| + resource.should_update? do + #only retrieved once per instance + @service_resource == nil + end + + resource.to_update do + service_resource = self.service.object_mask(NetworkService.default_object_mask).getServiceResource + NetworkService.new(softlayer_client, service_resource) unless service_resource.empty? + end + end + + ## + # Retrieve the name of the SoftLayer_Network_Storage_Group_Type which describes this group. + # :call-seq: + # type(force_update=false) + sl_dynamic_attr :type do |resource| + resource.should_update? do + #only retrieved once per instance + @type == nil + end + + resource.to_update do + group_type = self.service.getGroupType + group_type["name"] + end + end + + ## + # Returns the service for interacting with this network storage through the network API + # + def service + softlayer_client[:Network_Storage_Group].object_with_id(self.id) + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Network_Storage_Group)" => [ + 'alias', + 'createDate', + 'id', + 'modifyDate' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/ObjectFilter.rb b/lib/softlayer/ObjectFilter.rb index bb22088..a4c0587 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,159 @@ 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 passed 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 + # If value is Enumerable, it is equivalent to calling is_contained_by + def self.is(value) + value.kind_of?(Enumerable) ? is_contained_by(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. + # If value is Enumerable, it is equivalent to calling is_not_contained_by + def self.is_not(value) + value.kind_of?(Enumerable) ? is_not_contained_by(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) + # Matches the given value in a case-insensitive way + def self.matches_ignoring_case(value) + filter_criteria('_=', value) end - # Matches is the value in the field does not exactly equal - # the value passed in. - def is_not(value) - ObjectFilterOperation.new('!=', value) + # Matches when the key path value is a date between the start and end dates provided + # Dates should be strings in '%m/%d/%Y %T' format or Date/DateTime instances + def self.is_between_dates(start_date, end_date) + { + 'operation' => 'betweenDate', + 'options' => [ + { + 'name' => 'startDate', + 'value' => [ start_date.kind_of?(Date) ? start_date.strftime('%m/%d/%Y %T') : DateTime.strptime(start_date.to_s, '%m/%d/%Y %T').strftime('%m/%d/%Y %T') ] + }, + { + 'name' => 'endDate', + 'value' => [ end_date.kind_of?(Date) ? end_date.strftime('%m/%d/%Y %T') : DateTime.strptime(end_date.to_s, '%m/%d/%Y %T').strftime('%m/%d/%Y %T') ] + } + ] + } + end + + # Matches when key path value is equal to one of the given values in the Enumerable + def self.is_contained_by(value) + raise "Expected an Enumerable value with a list of acceptable values that can be converted to strings" unless value.kind_of?(Enumerable) + + { + 'operation' => 'in', + 'options' => [ + { + 'name' => 'data', + 'value' => value.collect { |enum_val| enum_val.to_s } + } + ] + } + end + + # Matches when key path value is not equal to one of the given values in the Enumerable + def self.is_not_contained_by(value) + raise "Expected an Enumerable value with a list of acceptable values that can be converted to strings" unless value.kind_of?(Enumerable) + + { + 'operation' => 'not in', + 'options' => [ + { + 'name' => 'data', + 'value' => value.collect { |enum_val| enum_val.to_s } + } + ] + } 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 +277,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 cde8c6a..de6beea 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" @@ -58,7 +42,7 @@ def parse(mask_string) recognize_token(@tokenizer, :eos, "Extraneous text after object mask: ") - if property && (property.name != "mask" && propertyName != "filterMask") + if property && (property.name != "mask" && property.name != "filterMask") raise ObjectMaskParserError, "Object Mask must begin with a 'mask' or 'filterMask' root property" end @@ -154,4 +138,4 @@ def recognize_token(tokenizer, expected_type, error_string, &predicate) end end -end # Module SoftLaye \ No newline at end of file +end # Module SoftLayer 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..7eeabc1 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,11 +10,45 @@ 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 - + # + # DEPRECATION WARNING: The following configuration option keys have been deprecated and + # will be removed with the next major version: capacityRestrictionMaximum, capacityRestrictionMinimum, + # capacityRestrictionType, hourlyRecurringFee, laborFee, oneTimeFee, recurringFee, requiredCoreCount, setupFee + class ProductConfigurationOption < Struct.new(:capacity, :capacityRestrictionMaximum, :capacity_restriction_maximum, + :capacityRestrictionMinimum, :capacity_restriction_minimum, :capacityRestrictionType, :capacity_restriction_type, + :description, :hourlyRecurringFee, :hourly_recurring_fee, :laborFee, :labor_fee, :oneTimeFee, :one_time_fee, + :price_id, :recurringFee, :recurring_fee, :requiredCoreCount, :required_core_count, :setupFee, :setup_fee, :units) # Is it evil, or just incongruous to give methods to a struct? - # returns true if the configurtion option has no fees associated with it. + def initialize(package_item_data, price_item_data) + self.capacity = package_item_data['capacity'] + self.description = package_item_data['description'] + self.units = package_item_data['units'] + + #DEPRECATION WARNING: All these are deprecated and will be removed with the next major version, pleace use keys below + self.capacityRestrictionMaximum = price_item_data['capacityRestrictionMaximum'] ? price_item_data['capacityRestrictionMaximum'] : nil + self.capacityRestrictionMinimum = price_item_data['capacityRestrictionMinimum'] ? price_item_data['capacityRestrictionMinimum'] : nil + self.capacityRestrictionType = price_item_data['capacityRestrictionType'] ? price_item_data['capacityRestrictionType'] : nil + self.hourlyRecurringFee = price_item_data['hourlyRecurringFee'] ? price_item_data['hourlyRecurringFee'].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.price_id = price_item_data['id'] + self.recurringFee = price_item_data['recurringFee'] ? price_item_data['recurringFee'].to_f : 0.0 + self.requiredCoreCount = price_item_data['requiredCoreCount'] ? price_item_data['requiredCoreCount'] : nil + self.setupFee = price_item_data['setupFee'] ? price_item_data['setupFee'].to_f : 0.0 + + self.capacity_restriction_maximum = price_item_data['capacityRestrictionMaximum'] ? price_item_data['capacityRestrictionMaximum'] : nil + self.capacity_restriction_minimum = price_item_data['capacityRestrictionMinimum'] ? price_item_data['capacityRestrictionMinimum'] : nil + self.capacity_restriction_type = price_item_data['capacityRestrictionType'] ? price_item_data['capacityRestrictionType'] : nil + self.hourly_recurring_fee = price_item_data['hourlyRecurringFee'] ? price_item_data['hourlyRecurringFee'].to_f : 0.0 + self.labor_fee = price_item_data['laborFee'] ? price_item_data['laborFee'].to_f : 0.0 + self.one_time_fee = price_item_data['oneTimeFee'] ? price_item_data['oneTimeFee'].to_f : 0.0 + self.recurring_fee = price_item_data['recurringFee'] ? price_item_data['recurringFee'].to_f : 0.0 + self.required_core_count = price_item_data['requiredCoreCount'] ? price_item_data['requiredCoreCount'] : nil + self.setup_fee = price_item_data['setupFee'] ? price_item_data['setupFee'].to_f : 0.0 + end + + # returns true if the configuration option has no fees associated with it. def free? self.setupFee == 0 && self.laborFee == 0 && self.oneTimeFee == 0 && self.recurringFee == 0 && self.hourlyRecurringFee == 0 end @@ -44,22 +62,35 @@ def free? # of a ProductPackage object. There should not be a need to create instances # of this class directly. # - # This class rougly represents entities in the +SoftLayer_Product_Item_Category+ + # This class roughly represents entities in the +SoftLayer_Product_Item_Category+ # service. class ProductItemCategory < ModelBase include ::SoftLayer::DynamicAttribute - + + ## + # :attr_reader: category_code + # The categoryCode is a primary identifier for a particular + # category. It is a string like 'os' or 'ram' + sl_attr :category_code, 'categoryCode' + ## # :attr_reader: # The categoryCode is a primary identifier for a particular # category. It is a string like 'os' or 'ram' + # + # DEPRECATION WARNING: This attribute is deprecated in favor of category_code + # and will be removed in the next major release. sl_attr :categoryCode - + ## # :attr_reader: # The name of a category is a friendly, readable string sl_attr :name + ## + # Retrieve the product item configuration information + # :call-seq: + # configuration_options(force_update=false) sl_dynamic_attr :configuration_options do |config_opts| config_opts.should_update? do # only retrieved once per instance @@ -80,24 +111,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 ## @@ -134,4 +155,4 @@ def required?() return @is_required end end -end \ No newline at end of file +end diff --git a/lib/softlayer/ProductPackage.rb b/lib/softlayer/ProductPackage.rb index 1cca71c..c83eeb5 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' @@ -54,16 +38,19 @@ class ProductPackage < ModelBase include ::SoftLayer::DynamicAttribute ## + # :attr_reader: # A friendly, readable name for the package sl_attr :name ## + # :attr_reader: available_locations # The list of locations where this product package is available. - sl_attr :availableLocations + sl_attr :available_locations, 'regions' ## - # The set of product categories needed to make an order for this product package. - # + # Retrieve the set of product categories needed to make an order for this product package. + # :call-seq: + # configuration(force_update=false) sl_dynamic_attr :configuration do |resource| resource.should_update? do # only retrieved once per instance @@ -81,7 +68,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,59 +78,98 @@ 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).object_mask(@@categories_object_mask).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 + + ## + # Retrieve the full set of product categories contained in the package + # :call-seq: + # categories(force_update=false) + 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 configuration + @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.category_code == 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 ## # Requests a list (array) of ProductPackages whose key names match the - # one passed in. + # one passed in. key_name may be a string or array. # 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 +181,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,10 +215,22 @@ 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 + @@categories_object_mask = "mask[" + [ "groups.prices.capacityRestrictionMaximum", + "groups.prices.capacityRestrictionMinimum", + "groups.prices.capacityRestrictionType", + "groups.prices.requiredCoreCount" ].join(",") + "]" + def self.default_object_mask(root) - "#{root}[id,name,description,availableLocations.location]" + "#{root}[id,name,description,regions]" end end -end # SoftLayer \ No newline at end of file +end # SoftLayer diff --git a/lib/softlayer/Server.rb b/lib/softlayer/Server.rb index cb1737b..3f028a0 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: @@ -43,9 +28,17 @@ class Server < SoftLayer::ModelBase # The domain name SoftLayer has stored for the server sl_attr :domain + ## + # :attr_reader: fqdn + # A convenience attribute that combines the hostname and domain name + sl_attr :fqdn, 'fullyQualifiedDomainName' + ## # :attr_reader: # A convenience attribute that combines the hostname and domain name + # + # DEPRECATION WARNING: This attribute is deprecated in favor of fqdn + # and will be removed in the next major release. sl_attr :fullyQualifiedDomainName ## @@ -54,12 +47,12 @@ class Server < SoftLayer::ModelBase sl_attr :datacenter ## - # :attr_reader: + # :attr_reader: primary_public_ip # The IP address of the primary public interface for the server sl_attr :primary_public_ip, "primaryIpAddress" ## - # :attr_reader: + # :attr_reader: primary_private_ip # The IP address of the primary private interface for the server sl_attr :primary_private_ip, "primaryBackendIpAddress" @@ -68,6 +61,95 @@ class Server < SoftLayer::ModelBase # Notes about these server (for use by the customer) sl_attr :notes + ## + # The maximum network monitor query/response levels currently supported by the server + # :call-seq: + # network_monitor_levels(force_update=false) + sl_dynamic_attr :network_monitor_levels do |resource| + resource.should_update? do + @network_monitor_levels == nil + end + + resource.to_update do + NetworkMonitorLevels.new(self.service.getAvailableMonitoring) + end + end + + ## + # A lsst of configured network monitors. + # :call-seq: + # network_monitors(force_update=false) + sl_dynamic_attr :network_monitors do |resource| + resource.should_update? do + @network_monitors == nil + end + + resource.to_update do + network_monitors_data = self.service.object_mask(NetworkMonitor.default_object_mask).getNetworkMonitors + + network_monitors_data.map! do |network_monitor| + NetworkMonitor.new(softlayer_client, network_monitor) unless network_monitor.empty? + end + + network_monitors_data.compact + end + end + + ## + # :attr_reader: + # The list of user customers notified on monitoring failures + # :call-seq: + # notified_network_monitor_users(force_update=false) + sl_dynamic_attr :notified_network_monitor_users do |resource| + resource.should_update? do + #only retrieved once per instance + @notified_network_monitor_users == nil + end + + resource.to_update do + notified_network_monitor_users_data = self.service.object_mask("mask[userId]").getMonitoringUserNotification + + notified_network_monitor_users = notified_network_monitor_users_data.collect do |notified_network_monitor_user| + user_customer_service = softlayer_client[:User_Customer].object_with_id(notified_network_monitor_user['userId']) + user_customer_data = user_customer_service.object_mask(UserCustomer.default_object_mask).getObject + + UserCustomer.new(softlayer_client, user_customer_data) unless user_customer_data.empty? + end + + notified_network_monitor_users.compact + end + end + + ## + # Retrieve the primary network component + # :call-seq: + # primary_network_component(force_update=false) + 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 + + ## + # Retrieve all software installed on current server + # :call-seq: + # software(force_update=false) + sl_dynamic_attr :software do |software| + software.should_update? do + @software == nil + end + + software.to_update do + software_data = self.service.object_mask(Software.default_object_mask).getSoftwareComponents + software_data.collect { |sw| Software.new(self.softlayer_client, sw) unless sw.empty? }.compact + end + end + ## # Construct a server from the given client using the network data found in +network_hash+ # @@ -83,6 +165,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 reboot) 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 +203,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 +217,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 +227,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 +245,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 +256,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 +274,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..ed188f2 --- /dev/null +++ b/lib/softlayer/ServerFirewall.rb @@ -0,0 +1,264 @@ +#-- +# 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 roughly 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 + + ## + # Retrieve 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! + # :call-seq: + # rules(force_update=false) + 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 + + ## + # Retrieve the server that this firewall is attached to. The result may be + # either a bare metal or virtual server. + # :call-seq: + # protected_server(force_update=false) + 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 incoming 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| + ServerFirewall.new(softlayer_client, bare_metal_firewall_data) + } + + virtual_server_firewalls = virtual_firewalls_data.collect { |virtual_firewall_data| + ServerFirewall.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 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 d470c78..429e98b 100644 --- a/lib/softlayer/Service.rb +++ b/lib/softlayer/Service.rb @@ -1,25 +1,26 @@ +#-- # 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 'xmlrpc/client' +# utility routine for swapping constants without warnings. +def with_warnings(flag) + old_verbose, $VERBOSE = $VERBOSE, flag + yield +ensure + $VERBOSE = old_verbose +end + +with_warnings(nil) { + # enable parsing of "nil" values in structures returned from the API + XMLRPC::Config.const_set('ENABLE_NIL_PARSER', true) + # enable serialization of "nil" values in structures sent to the API + XMLRPC::Config.const_set('ENABLE_NIL_CREATE', 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 +31,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 +53,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 @@ -81,7 +72,7 @@ module SoftLayer # => {... lots of information here representing the list of open tickets ...} # class Service - # The name of the service that this object calls. Cannot be emtpy or nil. + # The name of the service that this object calls. Cannot be empty or nil. attr_reader :service_name attr_reader :client @@ -296,9 +287,9 @@ def to_ary def xmlrpc_client() if !@xmlrpc_client - @xmlrpc_client = XMLRPC::Client.new2(URI.join(@client.endpoint_url,@service_name).to_s) + @xmlrpc_client = XMLRPC::Client.new2(URI.join(@client.endpoint_url,@service_name).to_s, nil, @client.network_timeout) - # this is a workaround for a bug in later versions of the XML-RPC client in Ruby Core. + # This is a workaround for a bug in later versions of the XML-RPC client in Ruby Core. # see https://bugs.ruby-lang.org/issues/8182 @xmlrpc_client.http_header_extra = { "Accept-Encoding" => "identity", @@ -315,6 +306,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/Software.rb b/lib/softlayer/Software.rb new file mode 100644 index 0000000..c404920 --- /dev/null +++ b/lib/softlayer/Software.rb @@ -0,0 +1,353 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer Software instance provides information about software + # installed on a specific piece of hardware. + # + # This class roughly corresponds to the entity SoftLayer_Software_Component + # in the API. + # + class Software < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: manufacturer_activation_code + # The manufacturer code that is needed to activate a license. + sl_attr :manufacturer_activation_code, 'manufacturerActivationCode' + + ## + # :attr_reader: manufacturer_license_key + # A license key for this specific installation of software, if it is needed. + sl_attr :manufacturer_license_key, 'manufacturerLicenseInstance' + + ## + # Retrieve the manufacturer, name and version of a piece of software. + # :call-seq: + # description(force_update=false) + sl_dynamic_attr :description do |resource| + resource.should_update? do + #only retrieved once per instance + @description == nil + end + + resource.to_update do + description = self.service.getSoftwareDescription + description['longDescription'] + end + end + + ## + # Retrieve the name of this specific piece of software. + # :call-seq: + # name(force_update=false) + sl_dynamic_attr :name do |resource| + resource.should_update? do + #only retrieved once per instance + @name == nil + end + + resource.to_update do + description = self.service.getSoftwareDescription + description['name'] + end + end + + ## + # Retrieve the Username/Password pairs used for access to this Software Installation. + # :call-seq: + # passwords(force_update=false) + sl_dynamic_attr :passwords do |resource| + resource.should_update? do + #only retrieved once per instance + @passwords == nil + end + + resource.to_update do + passwords = self.service.getPasswords + passwords.collect { |password_data| SoftwarePassword.new(softlayer_client, password_data) } + end + end + + ## + # Adds specified username/password combination to current software instance + # + def add_user_password(username, password, options = {}) + raise ArgumentError, "The new password cannot be nil" unless password + raise ArgumentError, "The new username cannot be nil" unless username + raise ArgumentError, "The new password cannot be empty" if password.empty? + raise ArgumentError, "The new username cannot be empty" if username.empty? + + raise Exception, "Cannot add username password, a Software Password already exists for the provided username" if self.has_user_password?(username.to_s) + + add_user_pw_template = { + 'softwareId' => self['id'].to_i, + 'password' => password.to_s, + 'username' => username.to_s + } + + add_user_pw_template['notes'] = options['notes'].to_s if options.has_key?('notes') + add_user_pw_template['port'] = options['port'].to_i if options.has_key?('port') + + softlayer_client[:Software_Component_Password].createObject(add_user_pw_template) + + @passwords = nil + end + + ## + # Deletes specified username password from current software instance + # + # + # This is a final action and cannot be undone. + # the transaction will proceed immediately. + # + # Call it with extreme care! + def delete_user_password!(username) + user_password = self.passwords.select { |sw_pw| sw_pw.username == username.to_s } + + unless user_password.empty? + softlayer_client[:Software_Component_Password].object_with_id(user_password.first['id']).deleteObject + @passwords = nil + end + end + + ## + # Returns whether or not one of the Software Password instances pertains to the specified user + # + def has_user_password?(username) + self.passwords.map { |sw_pw| sw_pw.username }.include?(username) + end + + ## + # Retrieve a list of software from hardware devices. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include software from hardware matching this datacenter + # * +:description+ (string/array) - Include software that matches this description + # * +:domain+ (string/array) - Include software from hardware matching this domain + # * +:hardware_type+ (symbol) - Include software from hardware matching this hardware type + # * +:hostname+ (string/array) - Include software from hardware matching this hostname + # * +:manufacturer+ (string/array) - Include software that matches this manufacturer + # * +:name+ (string/array) - Include software that matches this name + # * +:username+ (string/array) - Include software that has software password matching this username + # + # You may use the following properties to provide hardware or software object filter instances: + # * +:hardware_object_filter+ (ObjectFilter) - Include software from hardware that matches the criteria of this object filter + # * +:software_object_filter+ (ObjectFilter) - Include software that matches the criteria of this object filter + # * +:software_object_mask+ (string) - Include software properties that matches the criteria of this object mask + # + def self.find_software_on_hardware(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? :hardware_object_filter) + hardware_object_filter = options_hash[:hardware_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless hardware_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + hardware_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_object_filter) + software_object_filter = options_hash[:software_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_object_filter = ObjectFilter.new() + end + + filter_label = { + :bare_metal_instance => "bareMetalInstances", + :hardware => "hardware", + :network_hardware => "networkHardware", + :router => "routers" + } + + option_to_filter_path = { + :hardware => { + :datacenter => lambda { |hardware_type| return [ filter_label[hardware_type], '.datacenter.name' ].join }, + :domain => lambda { |hardware_type| return [ filter_label[hardware_type], '.domain' ].join }, + :hostname => lambda { |hardware_type| return [ filter_label[hardware_type], '.hostname' ].join }, + :tags => lambda { |hardware_type| return [ filter_label[hardware_type], '.tagReferences.tag.name' ].join } + }, + :software => { + :description => "softwareComponents.softwareDescription.longDescription", + :manufacturer => "softwareComponents.softwareDescription.manufacturer", + :name => "softwareComponents.softwareDescription.name", + :username => "softwareComponents.passwords.username" + } + } + + if options_hash[:hardware_type] + unless filter_label.keys.include?(options_hash[:hardware_type]) + raise "Expected :bare_metal_instance, :hardware, :network_hardware, or :router for option :hardware_type in #{__method__}" + end + end + + option_to_filter_path[:hardware].keys.each do |option| + if options_hash[option] + hardware_object_filter.modify { |filter| filter.accept(option_to_filter_path[:hardware][option].call(options_hash[:hardware_type] || :hardware)).when_it is(options_hash[option]) } + end + end + + option_to_filter_path[:software].each do |option, filter_path| + software_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(hardware_object_filter) unless hardware_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + case options_hash[:hardware_type] + when :bare_metal_instance + hardware_data = account_service.getBareMetalInstances + when :hardware, nil + hardware_data = account_service.getHardware + when :network_hardware + hardware_data = account_service.getNetworkHardware + when :router + hardware_data = account_service.getRouters + end + + software = hardware_data.collect do |hardware| + hardware_service = softlayer_client[:Hardware].object_with_id(hardware['id']) + hardware_service = hardware_service.object_filter(software_object_filter) unless software_object_filter.empty? + hardware_service = hardware_service.object_mask(Software.default_object_mask) + hardware_service = hardware_service.object_mask(options_hash[:software_object_mask]) if options_hash[:software_object_mask] + + software_data = hardware_service.getSoftwareComponents + software_data.map { |software| Software.new(softlayer_client, software) unless software.empty? }.compact + end + + software.flatten + end + + ## + # Retrieve a list of software from virtual servers. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include software from virtual servers matching this datacenter + # * +:description+ (string/array) - Include software that matches this description + # * +:domain+ (string/array) - Include software from virtual servers matching this domain + # * +:hostname+ (string/array) - Include software from virtual servers matching this hostname + # * +:manufacturer+ (string/array) - Include software that matches this manufacturer + # * +:name+ (string/array) - Include software that matches this name + # * +:username+ (string/array) - Include software that has software password matching this username + # + # You may use the following properties to provide virtual server or software object filter instances: + # * +:virtual_server_object_filter+ (ObjectFilter) - Include software from virtual servers that matches the criteria of this object filter + # * +:software_object_filter+ (ObjectFilter) - Include software that matches the criteria of this object filter + # * +:software_object_mask+ (string) - Include software properties that matches the criteria of this object mask + # + def self.find_software_on_virtual_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 + + if(options_hash.has_key? :virtual_server_object_filter) + virtual_server_object_filter = options_hash[:virtual_server_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless virtual_server_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + virtual_server_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_object_filter) + software_object_filter = options_hash[:software_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_object_filter = ObjectFilter.new() + end + + option_to_filter_path = { + :software => { + :description => "softwareComponents.softwareDescription.longDescription", + :manufacturer => "softwareComponents.softwareDescription.manufacturer", + :name => "softwareComponents.softwareDescription.name", + :username => "softwareComponents.passwords.username" + }, + :virtual_server => { + :datacenter => "virtualGuests.datacenter.name", + :domain => "virtualGuests.domain", + :hostname => "virtualGuests.hostname", + :tags => "virtualGuests.tagReferences.tag.name" + } + } + + option_to_filter_path[:virtual_server].each do |option, filter_path| + virtual_server_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + option_to_filter_path[:software].each do |option, filter_path| + software_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(virtual_server_object_filter) unless virtual_server_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + virtual_server_data = account_service.getVirtualGuests + + software = virtual_server_data.collect do |virtual_server| + virtual_server_service = softlayer_client[:Virtual_Guest].object_with_id(virtual_server['id']) + virtual_server_service = virtual_server_service.object_filter(software_object_filter) unless software_object_filter.empty? + virtual_server_service = virtual_server_service.object_mask(Software.default_object_mask) + virtual_server_service = virtual_server_service.object_mask(options_hash[:software_object_mask]) if options_hash[:software_object_mask] + + software_data = virtual_server_service.getSoftwareComponents + software_data.map { |software| Software.new(softlayer_client, software) unless software.empty? }.compact + end + + software.flatten + end + + ## + # Returns the service for interacting with this software component through the network API + # + def service + softlayer_client[:Software_Component].object_with_id(self.id) + end + + ## + # Make an API request to SoftLayer and return the latest properties hash + # for this object. + # + def softlayer_properties(object_mask = nil) + my_service = self.service + + if(object_mask) + my_service = my_service.object_mask(object_mask) + else + my_service = my_service.object_mask(self.class.default_object_mask) + end + + my_service.getObject() + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Software_Component)" => [ + 'id', + 'manufacturerActivationCode', + 'manufacturerLicenseInstance' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/SoftwarePassword.rb b/lib/softlayer/SoftwarePassword.rb new file mode 100644 index 0000000..3f931e3 --- /dev/null +++ b/lib/softlayer/SoftwarePassword.rb @@ -0,0 +1,597 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer SoftwarePassword instance provides information about + # a user's password associated with a SoftLayer Software instance. + # + # This class roughly corresponds to the entity SoftLayer_Software_Component_Password + # in the API. + # + class SoftwarePassword < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: created_at + # The date this username/password pair was created. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # The date this username/password pair was created. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: modified_at + # The date of the last modification to this username/password pair. + sl_attr :modified_at, 'modifyDate' + + ## + # :attr_reader: modified + # The date of the last modification to this username/password pair. + # DEPRECATION WARNING: This attribute is deprecated in favor of modified_at + # and will be removed in the next major release. + sl_attr :modified, 'modifyDate' + + ## + # :attr_reader: + # A note string stored for this username/password pair. + sl_attr :notes + + ## + # :attr_reader: + # The password part of the username/password pair. + sl_attr :password + + ## + # :attr_reader: + sl_attr :port + + ## + # The username part of the username/password pair. + sl_attr :username + + ## + # Updates the password for the current software user. + # + def password=(password) + raise ArgumentError, "The new password cannot be nil" unless password + raise ArgumentError, "The new password cannot be empty" if password.empty? + + self.service.editObject({ "password" => password.to_s }) + self.refresh_details() + end + + ## + # Retrieve a list of software passwords from application delivery controllers. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include software passwords from application delivery controllers matching this datacenter + # * +:name+ (string/array) - Include software passwords from application delivery controllers that matches this name + # * +:tags+ (string/array - Include software passwords from application delivery controllers that matches these tags + # * +:username+ (string/array) - Include software passwords that match this username + # + # Additionally you may provide options related to the request itself: + # * *:application_delivery_controller_object_filter* (ObjectFilter) - Include software passwords from application delivery controllers + # that matches the criteria of this object filter + # * *:software_password_object_filter* (ObjectFilter) - Include software passwords that match the criteria of this object filter + # * *:software_password_object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # + def self.find_passwords_for_application_delivery_controllers(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? :application_delivery_controller_object_filter) + application_delivery_controller_object_filter = options_hash[:application_delivery_controller_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless application_delivery_controller_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + application_delivery_controller_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_password_object_filter) + software_password_object_filter = options_hash[:software_password_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_password_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_password_object_filter = ObjectFilter.new() + end + + option_to_filter_path = { + :app_deliv_controller => { + :advanced_mode => "applicationDeliveryControllers.advancedModeFlag", + :datacenter => "applicationDeliveryControllers.datacenter.name", + :name => "applicationDeliveryControllers.name", + :tags => "applicationDeliveryControllers.tagReferences.tag.name" + }, + :software_password => { + :username => "password.username" + } + } + + application_delivery_controller_object_filter.modify { |filter| filter.accept(option_to_filter_path[:app_deliv_controller][:advanced_mode]).when_it is(true) } + + option_to_filter_path[:app_deliv_controller].each do |option, filter_path| + next if option == :advanced_mode + + if options_hash[option] + application_delivery_controller_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } + end + end + + option_to_filter_path[:software_password].each do |option, filter_path| + software_password_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(application_delivery_controller_object_filter) unless application_delivery_controller_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + application_delivery_controller_data = account_service.getApplicationDeliveryControllers + software_passwords = application_delivery_controller_data.collect do |application_delivery_controller| + application_delivery_controller_service = softlayer_client[:Network_Application_Delivery_Controller].object_with_id(application_delivery_controller['id']) + application_delivery_controller_service = application_delivery_controller_service.object_filter(software_password_object_filter) unless software_password_object_filter.empty? + application_delivery_controller_service = application_delivery_controller_service.object_mask(SoftwarePassword.default_object_mask) + application_delivery_controller_service = application_delivery_controller_service.object_mask(options_hash[:software_password_object_mask]) if options_hash[:software_password_object_mask] + + software_password_data = application_delivery_controller_service.getPassword + SoftwarePassword.new(softlayer_client, software_password_data) unless software_password_data.empty? + end + + software_passwords.compact + end + + ## + # Retrieve a list of software passwords from vlan firewalls management credentials. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include software passwords from vlan firewalls matching this datacenter + # * +:vlan_names+ (string/array) - Include software passwords from vlans that matches these names + # * +:vlan_numbers+ (string/array) - Include software passwords from vlans that matches these numbers + # * +:vlan_space+ (symbol) - Include software passwords from vlans that match this space + # * +:vlan_tags+ (string/array) - Include software passwords from vlans that matches these tags + # * +:vlan_fw_fqdn+ (string/array) - Include software passwords from vlan firewalls that match this fqdn + # * +:vlan_fw_tags+ (string/array) - Include software passwords from vlan firewalls that matches these tags + # * +:vlan_fw_type+ (string/array) - Include software passwords from vlan firewalls that match this type + # * +:username+ (string/array) - Include software passwords that match this username + # + # Additionally you may provide options related to the request itself: + # * *:software_password_object_filter* (ObjectFilter) - Include software passwords that match the criteria of this object filter + # * *:software_password_object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + # * *:vlan_firewall_object_filter* (ObjectFilter) - Include software passwords from vlan firewalls that match the + # criteria of this object filter + # * *:vlan_object_filter* (ObjectFilter) - Include software passwords from vlan firewalls whose vlans match the + # criteria of this object filter + # + def self.find_passwords_for_vlan_firewalls(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? :vlan_object_filter) + vlan_object_filter = options_hash[:vlan_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless vlan_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + vlan_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :vlan_firewall_object_filter) + vlan_firewall_object_filter = options_hash[:vlan_firewall_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless vlan_firewall_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + vlan_firewall_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_password_object_filter) + software_password_object_filter = options_hash[:software_password_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_password_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_password_object_filter = ObjectFilter.new() + end + + filter_label = { + :all => 'networkVlans', + :private => 'privateNetworkVlans', + :public => 'publicNetworkVlans' + } + + option_to_filter_path = { + :software_password => { + :username => "managementCredentials.username" + }, + :vlan => { + :vlan_dedicated_fw => lambda { |vlan_space| return [ filter_label[vlan_space], '.', 'dedicatedFirewallFlag' ].join }, + :vlan_names => lambda { |vlan_space| return [ filter_label[vlan_space], '.', 'name' ].join }, + :vlan_numbers => lambda { |vlan_space| return [ filter_label[vlan_space], '.', 'vlanNumber' ].join }, + :vlan_tags => lambda { |vlan_space| return [ filter_label[vlan_space], '.', 'tagReferences.tag.name' ].join } + }, + :vlan_firewall => { + :vlan_fw_datacenter => "networkVlanFirewall.datacenter.name", + :vlan_fw_fqdn => "networkVlanFirewall.fullyQualifiedDomainName", + :vlan_fw_tags => "networkVlanFirewall.tagReferences.tag.name", + :vlan_fw_type => "networkVlanFirewall.firewallType" + } + } + + if options_hash[:vlan_space] && ! filter_label.keys.include?(options_hash[:vlan_space]) + raise "Expected one of :all, :private, or :public for option :vlan_space in #{__method__}" + end + + option_to_filter_path[:software_password].each do |option, filter_path| + software_password_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + vlan_space = options_hash[:vlan_space] || :all + + option_to_filter_path[:vlan].keys.each do |option| + vlan_object_filter.modify { |filter| filter.accept(option_to_filter_path[:vlan][option].call(vlan_space)).when_it is(1) } if option == :vlan_dedicated_fw + + if options_hash[option] && option != :vlan_dedicated_fw + vlan_object_filter.modify { |filter| filter.accept(option_to_filter_path[:vlan][option].call(vlan_space)).when_it is(options_hash[option]) } + end + end + + option_to_filter_path[:vlan_firewall].each do |option, filter_path| + vlan_firewall_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(vlan_object_filter) unless vlan_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + case vlan_space + when :all + vlan_data = account_service.getNetworkVlans + when :private + vlan_data = account_service.getPrivateNetworkVlans + when :public + vlan_data = account_service.getPublicNetworkVlans + end + + vlan_fw_passwords = vlan_data.collect do |vlan| + vlan_service = softlayer_client[:Network_Vlan].object_with_id(vlan['id']) + vlan_service = vlan_service.object_filter(vlan_firewall_object_filter) unless vlan_firewall_object_filter.empty? + vlan_service = vlan_service.object_mask("mask[id]") + + vlan_fw = vlan_service.getNetworkVlanFirewall + + unless vlan_fw.empty? + vlan_fw_service = softlayer_client[:Network_Vlan_Firewall].object_with_id(vlan_fw['id']) + vlan_fw_service = vlan_fw_service.object_filter(software_password_object_filter) unless software_password_object_filter.empty? + vlan_fw_service = vlan_fw_service.object_mask(SoftwarePassword.default_object_mask) + vlan_fw_service = vlan_fw_service.object_mask(options_hash[:software_password_object_mask]) if options_hash[:software_password_object_mask] + + vlan_fw_password_data = vlan_fw_service.getManagementCredentials + SoftwarePassword.new(softlayer_client, vlan_fw_password_data) unless vlan_fw_password_data.empty? + end + end + + vlan_fw_passwords.compact + end + + ## + # Retrieve a list of software passwords from software on hardware devices. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include software passwords from software on hardware matching this datacenter + # * +:description+ (string/array) - Include software passwords from software that matches this description + # * +:domain+ (string/array) - Include software passwords from software on hardware matching this domain + # * +:hardware_type+ (symbol) - Include software passwords from software on hardware matching this hardware type + # * +:hostname+ (string/array) - Include software passwords from software on hardware matching this hostname + # * +:manufacturer+ (string/array) - Include software passwords from software that matches this manufacturer + # * +:name+ (string/array) - Include software passwords from software that matches this name + # * +:username+ (string/array) - Include software passwords for username matching this username + # + # You may use the following properties to provide hardware or software object filter instances: + # * +:hardware_object_filter+ (ObjectFilter) - Include software passwords from software on hardware that matches the criteria of this object filter + # * +:software_object_filter+ (ObjectFilter) - Include software passwords from software that matches the criteria of this object filter + # * +:software_password_object_filter* (ObjectFilter) - Include software passwords that match the criteria of this object filter + # * +:software_password_object_mask+ (string) - Include software password properties that matches the criteria of this object mask + # + def self.find_passwords_for_software_on_hardware(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? :hardware_object_filter) + hardware_object_filter = options_hash[:hardware_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless hardware_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + hardware_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_object_filter) + software_object_filter = options_hash[:software_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_password_object_filter) + software_password_object_filter = options_hash[:software_password_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_password_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_password_object_filter = ObjectFilter.new() + end + + filter_label = { + :bare_metal_instance => "bareMetalInstances", + :hardware => "hardware", + :network_hardware => "networkHardware", + :router => "routers" + } + + option_to_filter_path = { + :hardware => { + :datacenter => lambda { |hardware_type| return [ filter_label[hardware_type], '.datacenter.name' ].join }, + :domain => lambda { |hardware_type| return [ filter_label[hardware_type], '.domain' ].join }, + :hostname => lambda { |hardware_type| return [ filter_label[hardware_type], '.hostname' ].join }, + :tags => lambda { |hardware_type| return [ filter_label[hardware_type], '.tagReferences.tag.name' ].join } + }, + :software => { + :description => "softwareComponents.softwareDescription.longDescription", + :manufacturer => "softwareComponents.softwareDescription.manufacturer", + :name => "softwareComponents.softwareDescription.name", + :username => "softwareComponents.passwords.username" + }, + :software_password => { + :username => "passwords.username" + } + } + + if options_hash[:hardware_type] + unless filter_label.keys.include?(options_hash[:hardware_type]) + raise "Expected :bare_metal_instance, :hardware, :network_hardware, or :router for option :hardware_type in #{__method__}" + end + end + + option_to_filter_path[:hardware].keys.each do |option| + if options_hash[option] + hardware_object_filter.modify { |filter| filter.accept(option_to_filter_path[:hardware][option].call(options_hash[:hardware_type] || :hardware)).when_it is(options_hash[option]) } + end + end + + option_to_filter_path[:software].each do |option, filter_path| + software_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + option_to_filter_path[:software_password].each do |option, filter_path| + software_password_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(hardware_object_filter) unless hardware_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + case options_hash[:hardware_type] + when :bare_metal_instance + hardware_data = account_service.getBareMetalInstances + when :hardware, nil + hardware_data = account_service.getHardware + when :network_hardware + hardware_data = account_service.getNetworkHardware + when :router + hardware_data = account_service.getRouters + end + + software_passwords = hardware_data.collect do |hardware| + hardware_service = softlayer_client[:Hardware].object_with_id(hardware['id']) + hardware_service = hardware_service.object_filter(software_object_filter) unless software_object_filter.empty? + hardware_service = hardware_service.object_mask("mask[id]") + + software_data = hardware_service.getSoftwareComponents + + software_data.collect do |software| + next if software.empty? + + software_service = softlayer_client[:Software_Component].object_with_id(software['id']) + software_service = software_service.object_filter(software_password_object_filter) unless software_password_object_filter.empty? + software_service = software_service.object_mask(SoftwarePassword.default_object_mask) + software_service = software_service.object_mask(options_hash[:software_password_object_mask]) if options_hash[:software_password_object_mask] + + software_passwords_data = software_service.getPasswords + software_passwords_data.map { |password| SoftwarePassword.new(softlayer_client, password) unless password.empty? }.compact + end + end + + software_passwords.flatten + end + + ## + # Retrieve a list of software passwords from software virtual servers. + # + # 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. + # + # You may filter the list returned by adding options: + # * +:datacenter+ (string/array) - Include software passwords from software on virtual servers matching this datacenter + # * +:description+ (string/array) - Include software passwords from software that matches this description + # * +:domain+ (string/array) - Include software passwords from software on virtual servers matching this domain + # * +:hostname+ (string/array) - Include software passwords from software on virtual servers matching this hostname + # * +:manufacturer+ (string/array) - Include software passwords from software that matches this manufacturer + # * +:name+ (string/array) - Include software passwords from software that matches this name + # * +:username+ (string/array) - Include software passwords for username matching this username + # + # You may use the following properties to provide virtual server or software object filter instances: + # * +:virtual_server_object_filter+ (ObjectFilter) - Include software passwords from software on virtual servers that matches the criteria of this object filter + # * +:software_object_filter+ (ObjectFilter) - Include software passwords from softwarethat matches the criteria of this object filter + # * +:software_password_object_filter* (ObjectFilter) - Include software passwords that match the criteria of this object filter + # * +:software_password_object_mask+ (string) - Include software password properties that matches the criteria of this object mask + # + def self.find_passwords_for_software_on_virtual_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 + + if(options_hash.has_key? :virtual_server_object_filter) + virtual_server_object_filter = options_hash[:virtual_server_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless virtual_server_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + virtual_server_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_object_filter) + software_object_filter = options_hash[:software_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_object_filter = ObjectFilter.new() + end + + if(options_hash.has_key? :software_password_object_filter) + software_password_object_filter = options_hash[:software_password_object_filter] + raise "Expected an instance of SoftLayer::ObjectFilter" unless software_password_object_filter.kind_of?(SoftLayer::ObjectFilter) + else + software_password_object_filter = ObjectFilter.new() + end + + option_to_filter_path = { + :software => { + :description => "softwareComponents.softwareDescription.longDescription", + :manufacturer => "softwareComponents.softwareDescription.manufacturer", + :name => "softwareComponents.softwareDescription.name", + :username => "softwareComponents.passwords.username" + }, + :virtual_server => { + :datacenter => "virtualGuests.datacenter.name", + :domain => "virtualGuests.domain", + :hostname => "virtualGuests.hostname", + :tags => "virtualGuests.tagReferences.tag.name" + }, + :software_password => { + :username => "passwords.username" + } + } + + option_to_filter_path[:virtual_server].each do |option, filter_path| + virtual_server_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + option_to_filter_path[:software].each do |option, filter_path| + software_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + option_to_filter_path[:software_password].each do |option, filter_path| + software_password_object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option]) } if options_hash[option] + end + + account_service = softlayer_client[:Account] + account_service = account_service.object_filter(virtual_server_object_filter) unless virtual_server_object_filter.empty? + account_service = account_service.object_mask("mask[id]") + + virtual_server_data = account_service.getVirtualGuests + + software_passwords = virtual_server_data.collect do |virtual_server| + virtual_server_service = softlayer_client[:Virtual_Guest].object_with_id(virtual_server['id']) + virtual_server_service = virtual_server_service.object_filter(software_object_filter) unless software_object_filter.empty? + virtual_server_service = virtual_server_service.object_mask("mask[id]") + + software_data = virtual_server_service.getSoftwareComponents + software_data.collect do |software| + next if software.empty? + + software_service = softlayer_client[:Software_Component].object_with_id(software['id']) + software_service = software_service.object_filter(software_password_object_filter) unless software_password_object_filter.empty? + software_service = software_service.object_mask(SoftwarePassword.default_object_mask) + software_service = software_service.object_mask(options_hash[:software_password_object_mask]) if options_hash[:software_password_object_mask] + + software_passwords_data = software_service.getPasswords + software_passwords_data.map { |password| SoftwarePassword.new(softlayer_client, password) unless password.empty? }.compact + end + end + + software_passwords.flatten + end + + ## + # Update the passwords for a list of software passwords + # + # 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. + # + def self.update_passwords(passwords, password, 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 + + raise ArgumentError, "The new password cannot be nil" unless password + raise ArgumentError, "The new password cannot be empty" if password.empty? + + if ! passwords.kind_of?(Array) || ! passwords.select { |password| ! password.kind_of?(SoftLayer::SoftwarePassword) }.empty? + raise ArgumentError, "Expected an array of SoftLayer::SoftwarePassword instances" + end + + software_password_service = softlayer_client[:Software_Component_Password] + software_password_service.editObjects(passwords.map { |pw| { 'id' => pw['id'], 'password' => password.to_s } }) + end + + ## + # Returns the service for interacting with this software component password through the network API + # + def service + softlayer_client[:Software_Component_Password].object_with_id(self.id) + end + + ## + # Make an API request to SoftLayer and return the latest properties hash + # for this object. + # + def softlayer_properties(object_mask = nil) + my_service = self.service + + if(object_mask) + my_service = my_service.object_mask(object_mask) + else + my_service = my_service.object_mask(self.class.default_object_mask) + end + + my_service.getObject() + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Software_Component_Password)" => [ + 'createDate', + 'id', + 'modifyDate', + 'notes', + 'password', + 'port', + 'username' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/Ticket.rb b/lib/softlayer/Ticket.rb index c7a9a05..84fff92 100644 --- a/lib/softlayer/Ticket.rb +++ b/lib/softlayer/Ticket.rb @@ -1,27 +1,11 @@ -# +#-- # 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 + class Ticket < SoftLayer::ModelBase ## # :attr_reader: @@ -32,43 +16,51 @@ 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: last_edited_at + # The date the ticket was last updated. + sl_attr :last_edited_at, 'lastEditDate' + ## # :attr_reader: - # The date the ticket was last updated. + # The date the ticket was last updated. + # + # DEPRECATION WARNING: This attribute is deprecated in favor of last_edited_at + # and will be removed in the next major release. 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 - ## - # Add an update to this ticket. - # - def update(body = nil) - self.service.edit(self.softlayer_hash, body) - end + ## + # Add an update to this ticket. + # + def update(body = nil) + self.service.edit(self.softlayer_hash, body) + end ## # 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 ## # Override from model base. Requests new details about the ticket # from the server. - def softlayer_properties(object_mask = nil) + def softlayer_properties(object_mask = nil) my_service = self.service if(object_mask) @@ -78,12 +70,12 @@ def softlayer_properties(object_mask = nil) end my_service.getObject() - end + end ## # Returns the default object mask,as a hash, that is used when # retrieving ticket information from the SoftLayer server. - def self.default_object_mask + def self.default_object_mask { "mask" => [ 'id', # This is an internal ticket ID, not the one usually seen in the portal @@ -99,48 +91,25 @@ 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 :-( ] - } - end + }.to_sl_object_mask + end ## # Queries the SoftLayer API to retrieve a list of the valid # ticket subjects. - def self.ticket_subjects(client = nil) - @ticket_subjects ||= nil + def self.ticket_subjects(client = nil) + @ticket_subjects ||= nil - if !@ticket_subjects + if !@ticket_subjects 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(); - 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 + @ticket_subjects = softlayer_client[:Ticket_Subject].getAllObjects(); end - open_tickets_data = softlayer_client["Account"].object_mask(object_mask).getOpenTickets - open_tickets_data.collect { |ticket_data| new(softlayer_client, ticket_data) } + @ticket_subjects end - + ## # Find the ticket with the given ID and return it # @@ -151,19 +120,20 @@ def self.open_tickets(options = {}) # 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.ticket_with_id(ticket_id, options = {}) + # Additionally you may provide options related to the request itself: + # * *:object_mask* (string) - The object mask of properties you wish to receive for the items returned. + # If not provided, the result will use the default object mask + def self.ticket_with_id(ticket_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 - if options.has_key?(:object_mask) - object_mask = options[:object_mask] - else - object_mask = default_object_mask.to_sl_object_mask - end + ticket_service = softlayer_client[:Ticket].object_with_id(ticket_id) + ticket_service = ticket_service.object_mask(default_object_mask.to_sl_object_mask) + ticket_service = ticket_service.object_mask(options[:object_mask]) if options[:object_mask] - ticket_data = softlayer_client["Ticket"].object_with_id(ticket_id).object_mask(object_mask).getObject() + ticket_data = ticket_service.getObject() - return new(softlayer_client, ticket_data) + return Ticket.new(softlayer_client, ticket_data) end ## @@ -192,8 +162,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,8 +173,8 @@ 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 -end \ No newline at end of file + end +end diff --git a/lib/softlayer/UserCustomer.rb b/lib/softlayer/UserCustomer.rb new file mode 100644 index 0000000..1489bc8 --- /dev/null +++ b/lib/softlayer/UserCustomer.rb @@ -0,0 +1,187 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer UserCustomer instance provides information + # relating to a single SoftLayer customer portal user + # + # This class roughly corresponds to the entity SoftLayer_User_Customer + # in the API. + # + class UserCustomer < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: alternate_phone + # A portal user's secondary phone number. + sl_attr :alternate_phone, 'alternatePhone' + + ## + # :attr_reader: created_at + # The date a portal user's record was created. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # The date a portal user's record was created. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: display_name + # The portal user's display name. + sl_attr :display_name, 'displayName' + + ## + # :attr_reader: + # A portal user's email address. + sl_attr :email + + ## + # :attr_reader: first_name + # A portal user's first name. + sl_attr :first_name, 'firstName' + + ## + # :attr_reader: last_name + # A portal user's last name. + sl_attr :last_name, 'lastName' + + ## + # :attr_reader: modified_at + # The date a portal user's record was last modified. + sl_attr :modified_at, 'modifyDate' + + ## + # :attr_reader: modified + # The date a portal user's record was last modified. + # DEPRECATION WARNING: This attribute is deprecated in favor of modified_at + # and will be removed in the next major release. + sl_attr :modified, 'modifyDate' + + ## + # :attr_reader: office_phone + # A portal user's office phone number. + sl_attr :office_phone, 'officePhone' + + ## + # :attr_reader: password_expires + # The expiration date for the user's password. + sl_attr :password_expires, 'passwordExpireDate' + + ## + # :attr_reader: status_changed + # The date a portal users record's last status change. + sl_attr :status_changed, 'statusDate' + + ## + # :attr_reader: + # A portal user's username. + sl_attr :username + + ## + # Retrieve a portal user's additional email addresses. + # These email addresses are contacted when updates are made to support tickets. + # :call-seq: + # additional_emails(force_update=false) + sl_dynamic_attr :additional_emails do |resource| + resource.should_update? do + #only retrieved once per instance + @additional_emails == nil + end + + resource.to_update do + additional_emails = self.service.getAdditionalEmails + additional_emails.collect { |additional_email| additional_email['email'] } + end + end + + ## + # Retrieve a portal user's API Authentication keys. + # There is a max limit of two API keys per user. + # :call-seq: + # api_authentication_keys(force_update=false) + sl_dynamic_attr :api_authentication_keys do |resource| + resource.should_update? do + #only retrieved once per instance + @api_authentication_keys == nil + end + + resource.to_update do + self.service.object_mask("mask[authenticationKey,ipAddressRestriction]").getApiAuthenticationKeys + end + end + + ## + # Retrieve the external authentication bindings that link an external identifier to a SoftLayer user. + # :call-seq: + # external_bindings(force_update=false) + sl_dynamic_attr :external_bindings do |resource| + resource.should_update? do + #only retrieved once per instance + @external_bindings == nil + end + + resource.to_update do + external_bindings = self.service.object_mask(UserCustomerExternalBinding.default_object_mask).getExternalBindings + external_bindings.collect { |external_binding| UserCustomerExternalBinding.new(softlayer_client, external_binding) } + end + end + + ## + # Retrieve the user customer associated with the specified username + # + def self.user_customer_with_username(username, 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 + raise "#{__method__} requires a user customer username but none was given" if !username || username.empty? + + user_customer_object_filter = ObjectFilter.new() + + user_customer_object_filter.modify { |filter| filter.accept('users.username').when_it is(username) } + + account_service = Account.account_for_client(softlayer_client).service + account_service = account_service.object_filter(user_customer_object_filter) + account_service = account_service.object_mask(UserCustomer.default_object_mask) + + user_customer_data = account_service.getUsers + + if user_customer_data.length == 1 + UserCustomer.new(softlayer_client, user_customer_data.first) + end + end + + ## + # Returns the service for interacting with this user customer through the network API + # + def service + softlayer_client[:User_Customer].object_with_id(self.id) + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_User_Customer)" => [ + 'alternatePhone', + 'createDate', + 'displayName', + 'email', + 'firstName', + 'id', + 'lastName', + 'modifyDate', + 'officePhone', + 'passwordExpireDate', + 'statusDate', + 'username' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/UserCustomerExternalBinding.rb b/lib/softlayer/UserCustomerExternalBinding.rb new file mode 100644 index 0000000..4957c5a --- /dev/null +++ b/lib/softlayer/UserCustomerExternalBinding.rb @@ -0,0 +1,110 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer UserCustomerExternalBinding instance provides information + # for a single user customer's external binding. + # + # This class roughly corresponds to the entity SoftLayer_User_Customer_External_Binding + # in the API. + # + class UserCustomerExternalBinding < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: + # The flag that determines whether the external binding is active will be + # used for authentication or not. + sl_attr :active + + ## + # :attr_reader: created_at + # The date that the external authentication binding was created. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # The date that the external authentication binding was created. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: + # The password used to authenticate the external id at an external + # authentication source. + sl_attr :password + + ## + # Retrieve an optional note for identifying the external binding. + # :call-seq: + # note(force_update=false) + sl_dynamic_attr :note do |resource| + resource.should_update? do + #only retrieved once per instance + @note == nil + end + + resource.to_update do + self.service.getNote + end + end + + ## + # Retrieve the user friendly name of a type of external authentication binding. + # :call-seq: + # type(force_update=false) + sl_dynamic_attr :type do |resource| + resource.should_update? do + #only retrieved once per instance + @type == nil + end + + resource.to_update do + type = self.service.getType + type['name'] + end + end + + ## + # Retrieve the user friendly name of an external binding vendor. + # :call-seq: + # vendor(force_update=false) + sl_dynamic_attr :vendor do |resource| + resource.should_update? do + #only retrieved once per instance + @vendor == nil + end + + resource.to_update do + vendor = self.service.getVendor + vendor['name'] + end + end + + ## + # Returns the service for interacting with this user customer external binding + # through the network API + # + def service + softlayer_client[:User_Customer_External_Binding].object_with_id(self.id) + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_User_Customer_External_Binding)" => [ + 'active', + 'createDate', + 'id', + 'password' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/VLANFirewall.rb b/lib/softlayer/VLANFirewall.rb new file mode 100644 index 0000000..995eb3c --- /dev/null +++ b/lib/softlayer/VLANFirewall.rb @@ -0,0 +1,311 @@ +#-- +# 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: vlan_number + # + # The number of the VLAN protected by this firewall. + sl_attr :vlan_number, 'vlanNumber' + + ## + # :attr_reader: VLAN_number + # + # The number of the VLAN protected by this firewall. + # + # DEPRECATION WARNING: This attribute is deprecated in favor of vlan_number + # and will be removed in the next major release. + sl_attr :VLAN_number, 'vlanNumber' + + ## + # Retrieve 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. + # :call-seq: + # rules(force_update=false) + 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 primary_router + return self['primaryRouter']['hostname'] + end + + ## + # Returns the name of the primary router the firewall is attached to. + # This is often a "customer router" in one of the datacenters. + # + # DEPRECATION WARNING: This method is deprecated in favor of primary_router + # and will be removed in the next major release. + def primaryRouter + return self['primaryRouter']['hostname'] + end + + ## + # The fully qualified domain name of the physical device the + # firewall is implemented by. + def fqdn + if self.has_sl_property?('networkVlanFirewall') + return self['networkVlanFirewall']['fullyQualifiedDomainName'] + else + return @softlayer_hash + end + end + + ## + # The fully qualified domain name of the physical device the + # firewall is implemented by. + # + # DEPRECATION WARNING: This method is deprecated in favor of fqdn + # and will be removed in the next major release. + 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 incoming 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 firewalls 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 diff --git a/lib/softlayer/VLANFirewallOrder.rb b/lib/softlayer/VLANFirewallOrder.rb new file mode 100644 index 0000000..42d2aab --- /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_availability 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/VirtualDiskImage.rb b/lib/softlayer/VirtualDiskImage.rb new file mode 100644 index 0000000..e8c7d07 --- /dev/null +++ b/lib/softlayer/VirtualDiskImage.rb @@ -0,0 +1,238 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer VirtualDiskImage instance provides information about software + # installed on a specific piece of hardware. + # + # This class roughly corresponds to the entity SoftLayer_Virtual_Disk_Image + # in the API. + # + class VirtualDiskImage < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # :attr_reader: + # A disk image's size measured in gigabytes. + sl_attr :capacity + + ## + # :attr_reader: + # A disk image's unique md5 checksum. + sl_attr :checksum + + ## + # :attr_reader: created_at + # The date a disk image was created. + sl_attr :created_at, 'createDate' + + ## + # :attr_reader: created + # The date a disk image was created. + # DEPRECATION WARNING: This attribute is deprecated in favor of created_at + # and will be removed in the next major release. + sl_attr :created, 'createDate' + + ## + # :attr_reader: + # A brief description of a virtual disk image. + sl_attr :description + + ## + # :attr_reader: modified_at + # The date a disk image was last modified. + sl_attr :modified_at, 'modifyDate' + + ## + # :attr_reader: modified + # The date a disk image was last modified. + # DEPRECATION WARNING: This attribute is deprecated in favor of modified_at + # and will be removed in the next major release. + sl_attr :modified, 'modifyDate' + + ## + # :attr_reader: + # A descriptive name used to identify a disk image to a user. + sl_attr :name + + ## + # :attr_reader: + # The unit of storage in which the size of the image is measured. + # Defaults to "GB" for gigabytes. + sl_attr :units + + ## + # :attr_reader: + # A disk image's unique ID on a virtualization platform. + sl_attr :uuid + + ## + # Retrieve coalesced disk images associated with this virtual disk image + # :call-seq: + # coalesced_disk_images(force_update=false) + sl_dynamic_attr :coalesced_disk_images do |resource| + resource.should_update? do + #only retrieved once per instance + @coalesced_disk_images == nil + end + + resource.to_update do + coalesced_disk_images = self.service.getCoalescedDiskImages + coalesced_disk_images.collect { |coalesced_disk_image| VirtualDiskImage.new(softlayer_client, coalesced_disk_image) } + end + end + + ## + # Retrieve local disk flag associated with virtual disk image + # :call-seq: + # local_disk(force_update=false) + sl_dynamic_attr :local_disk do |resource| + resource.should_update? do + #only retrieved once per instance + @local_disk == nil + end + + resource.to_update do + self.service.getLocalDiskFlag + end + end + + ## + # Retrieve metadata as to whether this disk image is meant for + # storage of custom user data supplied with a Cloud Computing Instance order. + # :call-seq: + # metadata(force_update=false) + sl_dynamic_attr :metadata do |resource| + resource.should_update? do + #only retrieved once per instance + @metadata == nil + end + + resource.to_update do + self.service.getMetadataFlag + end + end + + ## + # Retrieve the references to the software that resides on a disk image. + # :call-seq: + # software(force_update=false) + sl_dynamic_attr :software do |resource| + resource.should_update? do + #only retrieved once per instance + @software == nil + end + + resource.to_update do + software_references = self.service.object_mask(VirtualDiskImageSoftware.default_object_mask).getSoftwareReferences + software_references.collect { |software| VirtualDiskImageSoftware.new(softlayer_client, software) unless software.empty? }.compact + end + end + + ## + # Retrieve the original disk image that the current disk image was cloned from. + # :call-seq: + # source_disk_image(force_update=false) + sl_dynamic_attr :source_disk_image do |resource| + resource.should_update? do + #only retrieved once per instance + @source_disk_image == nil + end + + resource.to_update do + source_disk_image = self.service.object_mask(VirtualDiskImage.default_object_mask).getSourceDiskImage + VirtualDiskImage.new(softlayer_client, source_disk_image) unless source_disk_image.empty? + end + end + + ## + # Retrieve a brief description of a virtual disk image type's function. + # :call-seq: + # type_description(force_update=false) + sl_dynamic_attr :type_description do |resource| + resource.should_update? do + #only retrieved once per instance + @type_description == nil + end + + resource.to_update do + type = self.service.getType + type['description'] + end + end + + ## + # Retrieve a virtual disk image type's name. + # :call-seq: + # type_name(force_update=false) + sl_dynamic_attr :type_name do |resource| + resource.should_update? do + #only retrieved once per instance + @type_name == nil + end + + resource.to_update do + type = self.service.getType + type['name'] + end + end + + ## + # Returns the service for interacting with this virtual disk image through the network API + # + def service + softlayer_client[:Virtual_Disk_Image].object_with_id(self.id) + end + + ## + # Retrieve the virtual disk image with the given image ID from the API + # + # 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 image + # + def self.image_with_id(image_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 + + vdi_service = softlayer_client[:Virtual_Disk_Image] + vdi_service = vdi_service.object_mask(default_object_mask.to_sl_object_mask) + + if options.has_key?(:object_mask) + vdi_service = vdi_service.object_mask(options[:object_mask]) + end + + image_data = vdi_service.object_with_id(image_id).getObject() + + return VirtualDiskImage.new(softlayer_client, image_data) + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Virtual_Disk_Image)" => [ + 'capacity', + 'checksum', + 'createDate', + 'description', + 'id', + 'modifyDate', + 'name', + 'units', + 'uuid' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/VirtualDiskImageSoftware.rb b/lib/softlayer/VirtualDiskImageSoftware.rb new file mode 100644 index 0000000..9f4f1d0 --- /dev/null +++ b/lib/softlayer/VirtualDiskImageSoftware.rb @@ -0,0 +1,51 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + +module SoftLayer + ## + # Each SoftLayer VirtualDiskImageSoftware is a record that connects + # a computing instance's virtual disk images with software records. + # + # This class roughly corresponds to the entity SoftLayer_Virtual_Disk_Image_Software + # in the API. + # + class VirtualDiskImageSoftware < ModelBase + include ::SoftLayer::DynamicAttribute + + ## + # The manufacturer, name and version of a piece of software. + # + def description + self['softwareDescription']['longDescription'] + end + + ## + # The name of this specific piece of software. + # + def name + self['softwareDescription']['name'] + end + + ## + # The password for this specific virtual disk image software instance. + # + def passwords + self['passwords'] + end + + protected + + def self.default_object_mask + { + "mask(SoftLayer_Virtual_Disk_Image_Software)" => [ + 'id', + 'passwords[password,username]', + 'softwareDescription[longDescription,name]' + ] + }.to_sl_object_mask + end + end +end #SoftLayer diff --git a/lib/softlayer/VirtualServer.rb b/lib/softlayer/VirtualServer.rb index 54001a3..726cb5b 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 ## @@ -34,45 +16,87 @@ class VirtualServer < Server include ::SoftLayer::DynamicAttribute ## - # :attr_reader: - # A count of the nubmer of virtual processing cores allocated + # :attr_reader: cores + # A count of the number of virtual processing cores allocated # to the server. sl_attr :cores, 'maxCpu' + ## + # :attr_reader: provisioned_at + # The date the Virtual Server was provisioned. This attribute can be + # nil if the SoftLayer system has not yet finished provisioning the + # server (consequently this attribute is used by the #wait_until_ready + # method to determine when a server has been provisioned) + sl_attr :provisioned_at, 'provisionDate' + ## # :attr_reader: # The date the Virtual Server was provisioned. This attribute can be # nil if the SoftLayer system has not yet finished provisioning the # server (consequently this attribute is used by the #wait_until_ready # method to determine when a server has been provisioned) + # + # DEPRECATION WARNING: This attribute is deprecated in favor of provisioned_at + # and will be removed in the next major release. sl_attr :provisionDate + ## + # :attr_reader: active_transaction + # The active transaction (if any) for this virtual server. Transactions + # are used to make configuration changes to the server and only one + # transaction can be active at a time. + sl_attr :active_transaction, 'activeTransaction' + ## # :attr_reader: # The active transaction (if any) for this virtual server. Transactions # are used to make configuration changes to the server and only one # transaction can be active at a time. + # + # DEPRECATION WARNING: This attribute is deprecated in favor of active_transaction + # and will be removed in the next major release. sl_attr :activeTransaction + ## + # :attr_reader: block_devices + # Storage devices attached to the server. Storage may be local + # to the host running the Virtual Server, or it may be located + # on the SAN + sl_attr :block_devices, 'blockDevices' + ## # :attr_reader: # Storage devices attached to the server. Storage may be local # to the host running the Virtual Server, or it may be located # on the SAN + # + # DEPRECATION WARNING: This attribute is deprecated in favor of block_devices + # and will be removed in the next major release. sl_attr :blockDevices + ## + # :attr_reader: last_operating_system_reload + # The last operating system reload transaction that was + # run for this server. #wait_until_ready compares the + # ID of this transaction to the ID of the active transaction + # to determine if an OS reload is in progress. + sl_attr :last_operating_system_reload, 'lastOperatingSystemReload' + ## # :attr_reader: # The last operating system reload transaction that was # run for this server. #wait_until_ready compares the # ID of this transaction to the ID of the active transaction # to determine if an OS reload is in progress. + # + # DEPRECATION WARNING: This attribute is deprecated in favor of last_operating_system_reload + # and will be removed in the next major release. sl_attr :lastOperatingSystemReload ## - # A virtual server can find out about items that are - # available for upgrades. - # + # Retrieve information about items that are available for upgrades. + # :call-seq: + # upgrade_options(force_update=false) sl_dynamic_attr :upgrade_options do |resource| resource.should_update? do @upgrade_options == nil @@ -90,49 +114,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,13 +124,24 @@ 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. # - def capture_image(image_name, include_attached_storage = false, image_notes = nil) + # 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 + disk_filter = lambda { |disk| disk['device'] != '1' } if include_attached_storage disks = self.blockDevices.select(&disk_filter) - self.service.createArchiveTransaction(image_name, disks, notes) if disks && !disks.empty? + 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 ## @@ -180,8 +172,8 @@ def wait_until_ready(max_trials, wait_for_transactions = false, seconds_between_ has_os_reload = has_sl_property? :lastOperatingSystemReload has_active_transaction = has_sl_property? :activeTransaction - reloading_os = has_active_transaction && has_os_reload && (self.lastOperatingSystemReload['id'] == self.activeTransaction['id']) - provisioned = has_sl_property? :provisionDate + reloading_os = has_active_transaction && has_os_reload && (self.last_operating_system_reload['id'] == self.active_transaction['id']) + provisioned = has_sl_property?(:provisionDate) && ! self['provisionDate'].empty? # a server is ready when it is provisioned, not reloading the OS # (and if wait_for_transactions is true, when there are no active transactions). @@ -198,7 +190,7 @@ def wait_until_ready(max_trials, wait_for_transactions = false, seconds_between_ end ## - # Retrive the virtual server with the given server ID from the API + # Retrieve the virtual server with the given server ID from the API # # The options parameter should contain: # @@ -214,7 +206,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) @@ -237,39 +229,46 @@ def self.server_with_id(server_id, options = {}) # If no client can be found the routine will raise an error. # # You may filter the list returned by adding options: - # * +:hourly+ (boolean) - Include servers billed hourly in the list - # * +:monthly+ (boolean) - Include servers billed monthly in the list - # * +:tags+ (array) - an array of strings representing tags to search for on the instances - # * +:cpus+ (int) - return virtual servers with the given number of (virtual) CPUs - # * +:memory+ (int) - return servers with at least the given amount of memory (in MB. e.g. 4096 = 4GB) - # * +:hostname+ (string) - return servers whose hostnames match the query string given (see ObjectFilter::query_to_filter_operation) - # * +:domain+ (string) - filter servers to those whose domain matches the query string given (see ObjectFilter::query_to_filter_operation) - # * +:local_disk+ (boolean) - include servers that do, or do not, have local disk storage - # * +:datacenter+ (string) - find servers whose short data center name (e.g. dal05, sjc01) matches the query string given (see ObjectFilter::query_to_filter_operation) - # * +:nic_speed+ (int) - include servers with the given nic speed (in Mbps, usually 10, 100, or 1000) - # * +:public_ip+ (string) - return servers whose public IP address matches the query string given (see ObjectFilter::query_to_filter_operation) - # * +:private_ip+ (string) - same as :public_ip, but for private IP addresses + # * +:hourly+ (boolean) - Include servers billed hourly in the list + # * +:monthly+ (boolean) - Include servers billed monthly in the list + # * +:tags+ (string/array) - an array of strings representing tags to search for on the instances + # * +:cpus+ (int/array) - return virtual servers with the given number of (virtual) CPUs + # * +:memory+ (int/array) - return servers with at least the given amount of memory (in MB. e.g. 4096 = 4GB) + # * +:hostname+ (string/array) - return servers whose hostnames match the query string given (see ObjectFilter::query_to_filter_operation) + # * +:domain+ (string/array) - filter servers to those whose domain matches the query string given (see ObjectFilter::query_to_filter_operation) + # * +:local_disk+ (boolean) - include servers that do, or do not, have local disk storage + # * +:datacenter+ (string/array) - find servers whose short data center name (e.g. dal05, sjc01) matches the query string given (see ObjectFilter::query_to_filter_operation) + # * +:nic_speed+ (int/array) - include servers with the given nic speed (in Mbps, usually 10, 100, or 1000) + # * +:public_ip+ (string/array) - return servers whose public IP address matches the query string given (see ObjectFilter::query_to_filter_operation) + # * +:private_ip+ (string/array) - same as :public_ip, but for private IP addresses # # Additionally you may provide options related to the request itself: - # * +:object_mask+ (string) - A object mask of properties, in addition to the default properties, that you wish to retrieve for the servers - # * +:result_limit+ (hash with :limit, and :offset keys) - Limit the scope of results returned. + # * *:object_filter* (ObjectFilter) - Include servers that match the criteria of this object filter + # * +:object_mask+ (string) - A object mask of properties, in addition to the default properties, that you wish to retrieve for the servers + # * +:result_limit+ (hash with :limit, and :offset keys) - Limit the scope of results returned. # 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", - :memory => "virtualGuests.maxMemory", - :hostname => "virtualGuests.hostname", - :domain => "virtualGuests.domain", + :cores => "virtualGuests.maxCpu", + :memory => "virtualGuests.maxMemory", + :hostname => "virtualGuests.hostname", + :domain => "virtualGuests.domain", :local_disk => "virtualGuests.localDiskFlag", :datacenter => "virtualGuests.datacenter.name", - :nic_speed => "virtualGuests.networkComponents.maxSpeed", - :public_ip => "virtualGuests.primaryIpAddress", - :private_ip => "virtualGuests.primaryBackendIpAddress" + :nic_speed => "virtualGuests.networkComponents.maxSpeed", + :public_ip => "virtualGuests.primaryIpAddress", + :private_ip => "virtualGuests.primaryBackendIpAddress", + :tags => "virtualGuests.tagReferences.tag.name" } if options_hash.has_key?(:local_disk) then @@ -280,35 +279,18 @@ 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) - 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", { - 'operation' => 'in', - 'options' => [{ - 'name' => 'data', - 'value' => options_hash[:tags] - }] - } )); + object_filter.modify { |filter| filter.accept(filter_path).when_it is(options_hash[option])} if options_hash[option] 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) + account_service = account_service.object_mask(options_hash[:object_mask]) if options_hash[: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) + if options_hash[:result_limit] && options_hash[:result_limit][:offset] && options_hash[:result_limit][:limit] + account_service = account_service.result_limit(options_hash[:result_limit][:offset], options_hash[:result_limit][:limit]) end case @@ -355,34 +337,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 +end diff --git a/lib/softlayer/VirtualServerOrder.rb b/lib/softlayer/VirtualServerOrder.rb index 4675484..d0816a1 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 @@ -101,6 +84,12 @@ class VirtualServerOrder attr_accessor :private_vlan_id # String, The URI of a post provisioning script to run on this server once it is created + attr_accessor :provision_script_uri + + # String, The URI of a post provisioning script to run on this server once it is created + # + # DEPRECATION WARNING: This attribute is deprecated in favor of provision_script_uri + # and will be removed in the next major release. attr_accessor :provision_script_URI # Integer, The id of the public VLAN this server should join @@ -119,7 +108,11 @@ class VirtualServerOrder # Corresponds to +primaryBackendNetworkComponent.networkVlan.id+ in the +createObject+ documentation attr_accessor :user_metadata - # Create a new order that works thorugh the given client connection + # Hash, supplemental options - See https://sldn.softlayer.com/reference/datatypes/SoftLayer_Virtual_Guest_SupplementalCreateObjectOptions + # Corresponds to +supplementalCreateObjectOptions+ in the +createObject+ documentation + attr_accessor :supplementalCreateObjectOptions + + # Create a new order that works through the given client connection def initialize (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 @@ -136,7 +129,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 +142,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 +163,23 @@ def virtual_guest_template "hourlyBillingFlag" => !!@hourly } - template["dedicatedAccountHostOnlyFlag"] = true if @dedicated_host_only - template["privateNetworkOnlyFlag"] = true if @private_network_only - - template["datacenter"] = {"name" => @datacenter} 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 - - if @image_global_id - template["blockDeviceTemplateGroup"] = {"globalIdentifier" => @image_global_id} + template['dedicatedAccountHostOnlyFlag'] = true if @dedicated_host_only + template['privateNetworkOnlyFlag'] = true if @private_network_only + + 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['postInstallScriptUri'] = @provision_script_uri.to_s if @provision_script_uri + 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['sshKeys'] = @ssh_key_ids.collect { |ssh_key_id| {'id'=> ssh_key_id.to_i } } if @ssh_key_ids + template['supplementalCreateObjectOptions'] = @supplementalCreateObjectOptions if @supplementalCreateObjectOptions + + 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 +208,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 +222,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 + # Returns a list of the valid :os_reference_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/VirtualServerOrder_Package.rb b/lib/softlayer/VirtualServerOrder_Package.rb new file mode 100644 index 0000000..4c30594 --- /dev/null +++ b/lib/softlayer/VirtualServerOrder_Package.rb @@ -0,0 +1,179 @@ +#-- +# 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 a virtual server using a product package. + # + # Ordering a server using a product package is a more complex process than + # ordering with simple attributes (as is done by the VirtualServerServerOrder class). + # However with that complexity comes the the ability to specify the configuration + # of the server in exacting detail. + # + # To use this class, you first select a product package. The product package + # defines the base configuration of the server as well as the set of configuration + # options available for that server. To fully configure the server you must select + # the value for each configuration option. + # + # This class roughly Corresponds to the SoftLayer_Container_Product_Order_Virtual_Guest + # data type in the SoftLayer API + # + # http://sldn.softlayer.com/reference/datatypes/SoftLayer_Container_Product_Order_Virtual_Guest + # + class VirtualServerOrder_Package < Server + # The following properties are required in a server order. + + # The product package object (an instance of SoftLayer::ProductPackage) identifying the base + # configuration for the server. A Virtual Server product package is returned by + # SoftLayer::ProductPackage.virtual_server_package + attr_reader :package + + # 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). + attr_accessor :hostname + + # The domain of the server being created (i.e. 'softlayer.com' is the domain of sldn.softlayer.com) + attr_accessor :domain + + # The value of this property should be a hash. The keys of the hash are ProductItemCategory + # codes (like 'os' and 'ram') while the values may be Integers or Objects. The Integer values + # should be the +id+ of a +SoftLayer_Product_Item_Price+ representing the configuration option + # chosen for that category. Objects must respond to the +price_id+ message and return an integer + # that is the +id+ of a +SoftLayer_Product_Item_Price+. Instances of the ProductConfigurationOption + # class behave this way. + # + # At a minimum, the configuration_options should include entries for each of the categories + # required by the package (i.e. those returned from ProductPackage#required_categories) + attr_accessor :configuration_options + + # The following properties are optional, but allow further fine tuning of + # the server + + # Boolean, If true, an hourly server will be ordered, otherwise a monthly server will be ordered + # Corresponds to +useHourlyPricing+ in the SoftLayer_Container_Product_Order_Virtual_Guest container + # documentation + attr_accessor :hourly + + # An instance of the SoftLayer::ImageTemplate class. Represents the image template that should + # be installed on the server. + attr_accessor :image_template + + # Integer, The id of the public VLAN this server should join + # Corresponds to +primaryNetworkComponent.networkVlan.id+ in the +createObject+ documentation + attr_accessor :public_vlan_id + + # Integer, The id of the private VLAN this server should join + # Corresponds to +primaryBackendNetworkComponent.networkVlan.id+ in the +createObject+ documentation + attr_accessor :private_vlan_id + + # The URI of a script to execute on the server after it has been provisioned. This may be + # any object which accepts the to_s message. The resulting string will be passed to SoftLayer API. + attr_accessor :provision_script_uri + + # The URI of a script to execute on the server after it has been provisioned. This may be + # any object which accepts the to_s message. The resulting string will be passed to SoftLayer API. + # + # DEPRECATION WARNING: This attribute is deprecated in favor of provision_script_uri + # and will be removed in the next major release. + attr_accessor :provision_script_URI + + # 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() + attr_accessor :ssh_key_ids + + # String, User metadata associated with the instance + # Corresponds to +userData+ in the +SoftLayer_Virtual_Guest+ documentation + attr_accessor :user_metadata + + ## + # You initialize a VirtualServerOrder_Package by passing in the package that you + # are ordering from. + def initialize(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 + + @configuration_options = [] + @package = SoftLayer::ProductPackage.virtual_server_package(@softlayer_client) + end + + ## + # Present the order for verification by the SoftLayer ordering system. + # The order is verified, but not executed. This should not + # change the billing of your account. + # + # If you add a block to the method call, it will receive the product + # order template before it is sent to the API. You may **carefully** make + # changes to the template to provide specialized configuration. + # + def verify + product_order = virtual_server_order + product_order = yield product_order if block_given? + softlayer_client[:Product_Order].verifyOrder(product_order) + end + + ## + # Submit the order to be executed by the SoftLayer ordering system. + # If successful this will probably result in additional billing items + # applied to your account! + # + # If you add a block to the method call, it will receive the product + # order template before it is sent to the API. You may **carefully** make + # changes to the template to provide specialized configuration. + # + # The return value of this call is a product order receipt. After + # submitting the order, it will proceed to Sales for authorization. + # + def place_order! + product_order = virtual_server_order + product_order = yield product_order if block_given? + softlayer_client[:Product_Order].placeOrder(product_order) + end + + protected + + ## + # Construct and return a hash representing a +SoftLayer_Container_Product_Order_Virtual_Guest+ + # based on the configuration options given. + def virtual_server_order + product_order = { + 'packageId' => @package.id, + 'useHourlyPricing' => !!@hourly, + 'virtualGuests' => [{ + 'domain' => @domain, + 'hostname' => @hostname + }] + } + + #Note that the use of image_template and SoftLayer::ProductPackage os/guest_diskX configuration category + #item prices is mutually exclusive. + product_order['imageTemplateGlobalIdentifier'] = @image_template.global_id if @image_template + product_order['location'] = @datacenter.id if @datacenter + product_order['provisionScripts'] = [@provision_script_URI.to_s] if @provision_script_URI + product_order['provisionScripts'] = [@provision_script_uri.to_s] if @provision_script_uri + product_order['sshKeys'] = [{ 'sshKeyIds' => @ssh_key_ids }] if @ssh_key_ids + product_order['virtualGuests'][0]['userData'] = @user_metadata if @user_metadata + product_order['primaryNetworkComponent'] = { "networkVlan" => { "id" => @public_vlan_id.to_i } } if @public_vlan_id + product_order['primaryBackendNetworkComponent'] = { "networkVlan" => {"id" => @private_vlan_id.to_i } } if @private_vlan_id + + product_order['prices'] = @configuration_options.collect do |key, value| + if value.respond_to?(:price_id) + price_id = value.price_id + else + price_id = value.to_i + end + + { 'id' => price_id } + end + + product_order + end + end # VirtualServerOrder_Package +end # SoftLayer diff --git a/lib/softlayer/VirtualServerUpgradeOrder.rb b/lib/softlayer/VirtualServerUpgradeOrder.rb new file mode 100644 index 0000000..13d8f57 --- /dev/null +++ b/lib/softlayer/VirtualServerUpgradeOrder.rb @@ -0,0 +1,141 @@ +#-- +# 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_template = order_object + order_template = yield order_object if block_given? + @virtual_server.softlayer_client[:Product_Order].verifyOrder(order_template) + 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_template = order_object + order_template = yield order_object if block_given? + + @virtual_server.softlayer_client[:Product_Order].placeOrder(order_template) + 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 prices 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) + _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 cf2dd32..75dfb5f 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.1.1" # 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.2.3" # 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..7b91b57 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 @@ -84,7 +68,7 @@ def __sl_object_mask_properties_for_keys # object masks class Array # Returns a string representing the object mask content represented by the - # Array. Each value in the array is converted to its object mask eqivalent + # Array. Each value in the array is converted to its object mask equivalent # This routine is an implementation detail used in the conversion of hashes # to object mask strings. You should not have to call this method directly. def _to_sl_object_mask_property() diff --git a/lib/softlayer_api.rb b/lib/softlayer_api.rb index 70b20ca..92f62d7 100755 --- a/lib/softlayer_api.rb +++ b/lib/softlayer_api.rb @@ -1,45 +1,56 @@ -# +#-- # 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 'date' +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/AccountPassword' +require 'softlayer/NetworkMonitor' require 'softlayer/Server' require 'softlayer/BareMetalServer' require 'softlayer/BareMetalServerOrder' require 'softlayer/BareMetalServerOrder_Package' +require 'softlayer/ImageTemplate' +require 'softlayer/NetworkComponent' +require 'softlayer/NetworkMessageDelivery' +require 'softlayer/NetworkService' +require 'softlayer/NetworkStorageAllowedHost' +require 'softlayer/NetworkStorageCredential' +require 'softlayer/NetworkStorageGroup' +require 'softlayer/NetworkStorage' require 'softlayer/ProductPackage' require 'softlayer/ProductItemCategory' +require 'softlayer/ServerFirewall' +require 'softlayer/ServerFirewallOrder' +require 'softlayer/SoftwarePassword' +require 'softlayer/Software' +require 'softlayer/Ticket' +require 'softlayer/UserCustomerExternalBinding' +require 'softlayer/UserCustomer' +require 'softlayer/VirtualDiskImage' +require 'softlayer/VirtualDiskImageSoftware' require 'softlayer/VirtualServer' require 'softlayer/VirtualServerOrder' +require 'softlayer/VirtualServerOrder_Package' +require 'softlayer/VirtualServerUpgradeOrder' +require 'softlayer/VLANFirewall' +require 'softlayer/VLANFirewallOrder' diff --git a/pkg/.gitignore b/pkg/.gitignore deleted file mode 100644 index e69de29..0000000 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..94848fc 100644 --- a/softlayer_api.gemspec +++ b/softlayer_api.gemspec @@ -27,5 +27,8 @@ 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' + # Fixing the following gems' versions to avoid requiring + # Ruby 2.0. + s.add_development_dependency 'mime-types', '= 2.99.3' + s.add_development_dependency 'coveralls', '= 0.7.2' end diff --git a/spec/APIParameterFilter_spec.rb b/spec/APIParameterFilter_spec.rb index 5cc1b37..d7fcca9 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")) @@ -36,7 +20,7 @@ end it "rejects nil object masks" do - expect { filter.object_mask(nil) }.to raise_error + expect { filter.object_mask(nil) }.to raise_error(ArgumentError) end it "stores its value in server_object_id when called " do @@ -54,13 +38,13 @@ describe "#object_mask" do it "rejects nil object masks" do - expect { filter.object_mask(nil) }.to raise_error + expect { filter.object_mask(nil) }.to raise_error(ArgumentError) end it "rejects calls that pass things other than strings" do - 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 + expect { filter.object_mask(['anArray']) }.to raise_error(ArgumentError) + expect { filter.object_mask({"a" => "hash"}) }.to raise_error(ArgumentError) + expect { filter.object_mask(Object.new) }.to raise_error(ArgumentError) end it "accepts strings representing a property set" do @@ -89,12 +73,12 @@ describe "#object_filter" do it "rejects nil filters" do - expect { filter.object_filter(nil) }.to raise_error + expect { filter.object_filter(nil) }.to raise_error(ArgumentError) end 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..c5493df 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) @@ -125,7 +124,7 @@ describe "Account.account_for_client" do it "raises an error if there is no client available" do SoftLayer::Client.default_client = nil - expect {SoftLayer::Account.account_for_client}.to raise_error + expect {SoftLayer::Account.account_for_client}.to raise_error(RuntimeError) end it "uses the default client if one is available" do @@ -142,4 +141,4 @@ expect(mock_account.id).to be(12345) end end -end \ No newline at end of file +end diff --git a/spec/BareMetalServerOrder_Package_spec.rb b/spec/BareMetalServerOrder_Package_spec.rb index b8dcb2c..290b22e 100644 --- a/spec/BareMetalServerOrder_Package_spec.rb +++ b/spec/BareMetalServerOrder_Package_spec.rb @@ -39,26 +39,31 @@ 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 - expect(test_order.hardware_order['hardware']['hostname']).to be_nil + expect(test_order.hardware_order['hardware'][0]['hostname']).to be_nil test_order.hostname = "testhostname" - expect(test_order.hardware_order['hardware']['hostname']).to eq "testhostname" + expect(test_order.hardware_order['hardware'][0]['hostname']).to eq "testhostname" end it "places its :domain attribute into into the hardware template in the order" do - expect(test_order.hardware_order['hardware']['domain']).to be_nil + expect(test_order.hardware_order['hardware'][0]['domain']).to be_nil test_order.domain = "softlayer.com" - expect(test_order.hardware_order['hardware']['domain']).to eq "softlayer.com" + expect(test_order.hardware_order['hardware'][0]['domain']).to eq "softlayer.com" end it "places config options as prices 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..dcd6a8e 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")) @@ -46,72 +30,74 @@ it "raises an error if you try to create an order with no client" do SoftLayer::Client.default_client = nil - expect {SoftLayer::BareMetalServerOrder.new()}.to raise_error + expect {SoftLayer::BareMetalServerOrder.new()}.to raise_error(RuntimeError) 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 @@ -270,12 +259,12 @@ it "has options routines that raise if not given a client" do SoftLayer::Client.default_client = nil - expect { SoftLayer::BareMetalServerOrder.create_object_options() }.to raise_error - expect { SoftLayer::BareMetalServerOrder.datacenter_options() }.to raise_error - expect { SoftLayer::BareMetalServerOrder.core_options() }.to raise_error - expect { SoftLayer::BareMetalServerOrder.disk_options() }.to raise_error - expect { SoftLayer::BareMetalServerOrder.os_reference_code_options() }.to raise_error - expect { SoftLayer::BareMetalServerOrder.max_port_speed_options() }.to raise_error + expect { SoftLayer::BareMetalServerOrder.create_object_options() }.to raise_error(RuntimeError) + expect { SoftLayer::BareMetalServerOrder.datacenter_options() }.to raise_error(RuntimeError) + expect { SoftLayer::BareMetalServerOrder.core_options() }.to raise_error(RuntimeError) + expect { SoftLayer::BareMetalServerOrder.disk_options() }.to raise_error(RuntimeError) + expect { SoftLayer::BareMetalServerOrder.os_reference_code_options() }.to raise_error(RuntimeError) + expect { SoftLayer::BareMetalServerOrder.max_port_speed_options() }.to raise_error(RuntimeError) end end diff --git a/spec/BareMetalServer_spec.rb b/spec/BareMetalServer_spec.rb index 14591a7..f5745a5 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') @@ -64,4 +54,4 @@ fake_server = SoftLayer::BareMetalServer.new(mock_client, { "id" => 12345 }) fake_server.cancel!(:migrate_larger, 'moving on up!' ) end -end \ No newline at end of file +end diff --git a/spec/Client_spec.rb b/spec/Client_spec.rb index 10594e3..3e47dfc 100644 --- a/spec/Client_spec.rb +++ b/spec/Client_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')) @@ -57,42 +41,53 @@ expect(client.api_key).to eq 'fake_key' end - it 'raises an error if passed an empty user name' do - expect do - $SL_API_USERNAME = '' - client = SoftLayer::Client.new(:api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error + it 'produces empty auth headers if the username is empty' do + + $SL_API_USERNAME = '' + client = SoftLayer::Client.new(:api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + + $SL_API_USERNAME = 'good_username' + $SL_API_KEY = 'sample' + client = SoftLayer::Client.new(:username => '', :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + end + + it 'produces empty auth headers if the username is nil' do + $SL_API_USERNAME = nil + client = SoftLayer::Client.new(:username => nil, :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') - expect do - $SL_API_USERNAME = 'good_username' - $SL_API_KEY = 'sample' - client = SoftLayer::Client.new(:username => '', :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error + expect(client.authentication_headers.empty?).to be true end - it 'fails if the user name is nil' do - expect do - $SL_API_USERNAME = nil - client = SoftLayer::Client.new(:username => nil, :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error + it 'produces empty auth headers if the api_key is empty' do + $SL_API_KEY = '' + client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true + + client = SoftLayer::Client.new(:username => 'fake_user', :api_key => '', :endpoint_url => 'http://fakeurl.org/') + + expect(client.authentication_headers.empty?).to be true end - it 'fails if the api_key is empty' do - expect do - $SL_API_KEY = '' - client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error + it 'produces empty auth headers if the api_key is nil' do + $SL_API_KEY = nil + client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/', :api_key => nil) - expect do - client = SoftLayer::Client.new(:username => 'fake_user', :api_key => '', :endpoint_url => 'http://fakeurl.org/') - end.to raise_error + expect(client.authentication_headers.empty?).to be true end - it 'fails if the api_key is nil' do - expect do - $SL_API_KEY = nil - client = SoftLayer::Client.new(:username => 'fake_user', :endpoint_url => 'http://fakeurl.org/', :api_key => nil) - end.to raise_error + it 'initializes by default with nil as the timeout' do + client = SoftLayer::Client.new(:username => 'fake_user', :api_key => 'fake_key', :endpoint_url => 'http://fakeurl.org/') + expect(client.network_timeout).to be_nil + end + + it 'Accepts a timeout given as a config parameter' do + 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 @@ -140,8 +135,8 @@ end it "should reject empty or nil service names" do - expect { test_client.service_named('') }.to raise_error - expect { test_client.service_named(nil) }.to raise_error + expect { test_client.service_named('') }.to raise_error(ArgumentError) + expect { test_client.service_named(nil) }.to raise_error(ArgumentError) end it "should be able to construct a service" do @@ -152,32 +147,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..82332fe 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")) @@ -26,11 +10,91 @@ require 'softlayer_api' require 'rspec' +require 'tempfile' + describe SoftLayer::Config do + + before :each do + ENV.delete("SL_USERNAME") + ENV.delete("SL_API_KEY") + ENV.delete("SL_API_USER_AGENT") + ENV.delete("SL_PROFILE") + end + it "retrieves config information from environment variables" do ENV.store("SL_USERNAME", "PoohBear") ENV.store("SL_API_KEY", "DEADBEEFBADF00D") + ENV.store("SL_API_USER_AGENT", "de trackerz") - expect(SoftLayer::Config.environment_settings).to eq({ :username => "PoohBear", :api_key => "DEADBEEFBADF00D" }) + expect(SoftLayer::Config.environment_settings).to eq({ :user_agent => "de trackerz", :username => "PoohBear", :api_key => "DEADBEEFBADF00D" }) end + +it "retrieves the properties from a custom file" do + file = Tempfile.new('properties_from_file') + begin + file.puts("[softlayer]") + file.puts("username = PoohBear") + file.puts("api_key = DEADBEEFBADF00D") + file.puts("timeout = 40") + file.close + + settings = SoftLayer::Config.file_settings(file.path) + ensure + file.close + file.unlink + end + + expect(settings[:username]).to eq("PoohBear") + expect(settings[:api_key]).to eq("DEADBEEFBADF00D") + expect(settings[:timeout]).to eq(40) + end + + it "retrieves the properties from a custom file using a custom profile" do + ENV.store("SL_PROFILE", "softlayer_qa") + file = Tempfile.new('properties_from_file') + begin + file.puts("[softlayer_dev]") + file.puts("username = PoohBear") + file.puts("api_key = DEADBEEFBADF00D") + file.puts("timeout = 40") + file.puts("\n") + file.puts("[softlayer_qa]") + file.puts("username = Piglet") + file.puts("api_key = MOOOOOOOO") + file.puts("timeout = 60") + file.puts("\n") + file.puts("[softlayer_prod]") + file.puts("username = Eeyore") + file.puts("api_key = VEG_ALL_THE_WAY") + file.puts("timeout = 80") + file.close + + settings = SoftLayer::Config.file_settings(file.path) + ensure + file.close + file.unlink + end + + expect(settings[:username]).to eq("Piglet") + expect(settings[:api_key]).to eq("MOOOOOOOO") + expect(settings[:timeout]).to eq(60) + end + + it "retrieves the timeout field as an integer when presented as a string" do + file = Tempfile.new('config_test') + begin + file.puts("[softlayer]") + file.puts("username = PoohBear") + file.puts("api_key = DEADBEEFBADF00D") + file.puts("timeout = 40") + file.close + + settings = SoftLayer::Config.file_settings(file.path) + ensure + file.close + file.unlink + end + + expect(settings[:timeout]).to eq(40) + end end diff --git a/spec/Datacenter_spec.rb b/spec/Datacenter_spec.rb new file mode 100644 index 0000000..981cf40 --- /dev/null +++ b/spec/Datacenter_spec.rb @@ -0,0 +1,34 @@ +#-- +# 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::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..f5687f5 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")) @@ -147,8 +131,8 @@ class TestClass end it "raises an exception if passed an invalid name" do - expect { SoftLayer::DynamicAttribute::DynamicAttributeDefinition.new(nil) }.to raise_error - expect { SoftLayer::DynamicAttribute::DynamicAttributeDefinition.new("") }.to raise_error + expect { SoftLayer::DynamicAttribute::DynamicAttributeDefinition.new(nil) }.to raise_error(ArgumentError) + expect { SoftLayer::DynamicAttribute::DynamicAttributeDefinition.new("") }.to raise_error(ArgumentError) end it "has valid initial values" do 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..09c32b7 100644 --- a/spec/ObjectFilter_spec.rb +++ b/spec/ObjectFilter_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')) @@ -27,115 +11,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 - object_filter = SoftLayer::ObjectFilter.build("domain") { begins_with ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "^= value"} }) + it "defines the contains matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.contains(" 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 begins_with matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.begins_with(" 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 ends_with matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.ends_with(" fred ")).to eq({ 'operation' => '$= fred' }) + end - object_filter = SoftLayer::ObjectFilter.build("domain") { is_not ' value' } - expect(object_filter).to eq({ "domain" => { 'operation' => "!= value"} }) + 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_greater_than '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_less_than ' 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_or_equal_to ' 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_or_equal_to '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") { contains_exactly ' 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") { does_not_contain ' 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 + + it "defines the is_null matcher" do + expect(SoftLayer::ObjectFilterDefinitionContext.is_null()).to eq({ 'operation' => 'is null' }) + end + + 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 d2480f6..b736537 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")) @@ -50,10 +34,31 @@ end it "should reject extraeous text" do - expect { result = subject.parse("mask.simple, bob") }.to raise_error - expect { result = subject.parse("mask[two,children], bob") }.to raise_error + expect { result = subject.parse("mask.simple, bob") }.to raise_error(SoftLayer::ObjectMaskParserError) + expect { result = subject.parse("mask[two,children], bob") }.to raise_error(SoftLayer::ObjectMaskParserError) end + it "should parse a mask with fiterMask" do + result = nil + expect { result = subject.parse("filterMask.simple1") }.to_not raise_error + + 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 + + expect(result.count).to eq 2 + expect(result[0].name).to eq 'filterMask' + expect(result[0].children.count).to eq 1 + expect(result[0].children[0].name).to eq "simple1" + + 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 describe SoftLayer::ObjectMaskParser, "#parse_property_set" do @@ -71,12 +76,12 @@ it "should fail if missing the starting bracket" do tokenizer = SoftLayer::ObjectMaskTokenizer.new("propertyName]") - expect { sequence = subject.parse_property_set(tokenizer) }.to raise_error + expect { sequence = subject.parse_property_set(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end it "should fail if missing the ending bracket" do tokenizer = SoftLayer::ObjectMaskTokenizer.new("[propertyName") - expect { sequence = subject.parse_property_set(tokenizer) }.to raise_error + expect { sequence = subject.parse_property_set(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end end @@ -111,12 +116,12 @@ it "should reject an incomplete sequence" do tokenizer = SoftLayer::ObjectMaskTokenizer.new("propertyName,") - expect { sequence = subject.parse_property_sequence(tokenizer) }.to raise_error + expect { sequence = subject.parse_property_sequence(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end it "should reject an invalid property" do tokenizer = SoftLayer::ObjectMaskTokenizer.new("propertyName,bad_property") - expect { sequence = subject.parse_property_sequence(tokenizer) }.to raise_error + expect { sequence = subject.parse_property_sequence(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end end @@ -217,17 +222,17 @@ it "should fail if you try to provide a type list" do tokenizer = SoftLayer::ObjectMaskTokenizer.new("(Valid_Property_Type,Some_Other_Type)") - expect { subject.parse_property_type(tokenizer) }.to raise_error + expect { subject.parse_property_type(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end it "should fail if you leave off the first paren" do tokenizer = SoftLayer::ObjectMaskTokenizer.new("Valid_Property_Type)") - expect { subject.parse_property_type(tokenizer) }.to raise_error + expect { subject.parse_property_type(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end it "should fail if you leave off the last paren" do tokenizer = SoftLayer::ObjectMaskTokenizer.new("Valid_Property_Type[foo,bar]") - expect { subject.parse_property_type(tokenizer) }.to raise_error + expect { subject.parse_property_type(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end end @@ -252,4 +257,4 @@ tokenizer = SoftLayer::ObjectMaskTokenizer.new("*!snork") expect { subject.parse_property_type_name(tokenizer) }.to raise_error(SoftLayer::ObjectMaskParserError) end -end \ No newline at end of file +end 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..0572c31 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 @@ -87,10 +71,10 @@ it "calling with no client should raise" do SoftLayer::Client.default_client = nil - expect { SoftLayer::ProductPackage.packages_with_key_name('BARE_METAL_CORE') }.to raise_error - expect { SoftLayer::ProductPackage.virtual_server_package() }.to raise_error - expect { SoftLayer::ProductPackage.bare_metal_instance_package() }.to raise_error - expect { SoftLayer::ProductPackage.bare_metal_server_packages() }.to raise_error + expect { SoftLayer::ProductPackage.packages_with_key_name('BARE_METAL_CORE') }.to raise_error(RuntimeError) + expect { SoftLayer::ProductPackage.virtual_server_package() }.to raise_error(RuntimeError) + expect { SoftLayer::ProductPackage.bare_metal_instance_package() }.to raise_error(RuntimeError) + expect { SoftLayer::ProductPackage.bare_metal_server_packages() }.to raise_error(RuntimeError) end end -end \ No newline at end of file +end diff --git a/spec/ServerFirewall_spec.rb b/spec/ServerFirewall_spec.rb new file mode 100644 index 0000000..b4a36d4 --- /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(ArgumentError) + expect{ mock_firewall.change_rules_bypass!(false) }.to raise_error(ArgumentError) + expect{ mock_firewall.change_rules_bypass!(:route_around_firewall) }.to raise_error(ArgumentError) + expect{ mock_firewall.change_rules_bypass!(:route_through_firewall) }.to raise_error(ArgumentError) + expect{ mock_firewall.change_rules_bypass!("apply_firewall_rules") }.to raise_error(ArgumentError) + expect{ mock_firewall.change_rules_bypass!("bypass_firewall_rules") }.to raise_error(ArgumentError) + expect{ mock_firewall.change_rules_bypass!(nil) }.to raise_error(ArgumentError) + expect{ mock_firewall.change_rules_bypass!(1) }.to raise_error(ArgumentError) + expect{ mock_firewall.change_rules_bypass!(0) }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/Server_spec.rb b/spec/Server_spec.rb index 7276773..aa0615a 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 + expect { SoftLayer::Server.new(mock_client, { "id" => 12345 }) }.to raise_error(RuntimeError) end -end \ No newline at end of file +end diff --git a/spec/Service_spec.rb b/spec/Service_spec.rb index dd48fb7..2dcd08e 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")) @@ -41,9 +25,9 @@ end it "rejects a nil or empty service name" do - expect {service = SoftLayer::Service.new(nil)}.to raise_error - expect {service = SoftLayer::Service.new("")}.to raise_error - expect {service = SoftLayer::Service.new('')}.to raise_error + expect {service = SoftLayer::Service.new(nil)}.to raise_error(ArgumentError) + expect {service = SoftLayer::Service.new("")}.to raise_error(ArgumentError) + expect {service = SoftLayer::Service.new('')}.to raise_error(ArgumentError) end it "assigns the service name for the service" do @@ -77,18 +61,30 @@ client = SoftLayer::Client.new() # authentication is taken from the globals expect { SoftLayer::Service.new("SoftLayer_Account", :client => client, :username => "sample_username", :api_key => "blah") }.to raise_error(RuntimeError) end - end #describe #new end -describe SoftLayer::Service do +describe SoftLayer::Service, "xmlrpc client" do + 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] + xmlrpc = ticket_service.xmlrpc_client() + expect(xmlrpc.timeout).to eq 60 + end +end + +describe SoftLayer::Service, "parameter filters" do let (:service) do SoftLayer::Service.new("SoftLayer_Ticket", :username => "sample_username", :api_key => "blah") end 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 @@ -164,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 @@ -173,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..bc9bb5b 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) @@ -67,7 +40,7 @@ it "raises an error if you try to get ticket subjects with no client" do SoftLayer::Client.default_client = nil - expect {SoftLayer::Ticket.ticket_subjects() }.to raise_error + expect {SoftLayer::Ticket.ticket_subjects() }.to raise_error(RuntimeError) end it "identifies itself with the ticket service" do @@ -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 @@ -90,4 +63,4 @@ expect(ticket_service.server_object_id).to eq(12345) expect(ticket_service.target.service_name).to eq "SoftLayer_Ticket" end -end \ No newline at end of file +end diff --git a/spec/VLANFirewall_spec.rb b/spec/VLANFirewall_spec.rb new file mode 100644 index 0000000..18abd55 --- /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(NoMethodError) + expect{ mock_firewall.change_rules_bypass!(false) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_rules_bypass!(:route_around_firewall) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_rules_bypass!(:route_through_firewall) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_rules_bypass!("apply_firewall_rules") }.to raise_error(NoMethodError) + expect{ mock_firewall.change_rules_bypass!("bypass_firewall_rules") }.to raise_error(NoMethodError) + expect{ mock_firewall.change_rules_bypass!(nil) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_rules_bypass!(1) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_rules_bypass!(0) }.to raise_error(NoMethodError) + 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(NoMethodError) + expect{ mock_firewall.change_routing_bypass!(false) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_routing_bypass!(:apply_firewall_rules) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_routing_bypass!(:bypass_firewall_rules) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_routing_bypass!("route_around_firewall") }.to raise_error(NoMethodError) + expect{ mock_firewall.change_routing_bypass!("route_through_firewall") }.to raise_error(NoMethodError) + expect{ mock_firewall.change_routing_bypass!(nil) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_routing_bypass!(1) }.to raise_error(NoMethodError) + expect{ mock_firewall.change_routing_bypass!(0) }.to raise_error(NoMethodError) + end + end +end diff --git a/spec/VirtualServerOrder_spec.rb b/spec/VirtualServerOrder_spec.rb index d50995f..96b6edb 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")) @@ -46,54 +30,57 @@ it "raises an error if you try to create an order with no client" do SoftLayer::Client.default_client = nil - expect {SoftLayer::VirtualServerOrder.new()}.to raise_error + expect {SoftLayer::VirtualServerOrder.new()}.to raise_error(RuntimeError) 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,41 +140,47 @@ 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 + it "puts the bootMode attribute into the template as supplementalCreateObjectOptions.bootMode" do + expect(subject.virtual_guest_template['supplementalCreateObjectOptions']).to be_nil + subject.supplementalCreateObjectOptions = {"bootMode" => 'HVM'} + expect(subject.virtual_guest_template['supplementalCreateObjectOptions']).to eq ({"bootMode" => "HVM"}) + end + it "calls the softlayer API to validate an order template" do client = SoftLayer::Client.new(:username => "fakeusername", :api_key => 'DEADBEEFBADF00D') @@ -197,7 +190,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 +206,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 +222,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 +239,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 +250,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 +271,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 @@ -315,14 +311,14 @@ it "has options routines that raise if not given a client" do SoftLayer::Client.default_client = nil - expect { SoftLayer::VirtualServerOrder.create_object_options() }.to raise_error - expect { SoftLayer::VirtualServerOrder.datacenter_options() }.to raise_error - expect { SoftLayer::VirtualServerOrder.create_object_options() }.to raise_error - expect { SoftLayer::VirtualServerOrder.core_options() }.to raise_error - expect { SoftLayer::VirtualServerOrder.memory_options() }.to raise_error - expect { SoftLayer::VirtualServerOrder.disk_options() }.to raise_error - expect { SoftLayer::VirtualServerOrder.os_reference_code_options() }.to raise_error - expect { SoftLayer::VirtualServerOrder.max_port_speed_options() }.to raise_error + expect { SoftLayer::VirtualServerOrder.create_object_options() }.to raise_error(RuntimeError) + expect { SoftLayer::VirtualServerOrder.datacenter_options() }.to raise_error(RuntimeError) + expect { SoftLayer::VirtualServerOrder.create_object_options() }.to raise_error(RuntimeError) + expect { SoftLayer::VirtualServerOrder.core_options() }.to raise_error(RuntimeError) + expect { SoftLayer::VirtualServerOrder.memory_options() }.to raise_error(RuntimeError) + expect { SoftLayer::VirtualServerOrder.disk_options() }.to raise_error(RuntimeError) + expect { SoftLayer::VirtualServerOrder.os_reference_code_options() }.to raise_error(RuntimeError) + expect { SoftLayer::VirtualServerOrder.max_port_speed_options() }.to raise_error(RuntimeError) end end end diff --git a/spec/VirtualServerUpgradeOrder_spec.rb b/spec/VirtualServerUpgradeOrder_spec.rb new file mode 100644 index 0000000..52e74ba --- /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(RuntimeError) + expect { SoftLayer::VirtualServerUpgradeOrder.new("foo") }.to raise_error(RuntimeError) + 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 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")) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index aae6812..91d2754 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,9 @@ +#-- +# Copyright (c) 2014 SoftLayer Technologies, Inc. All rights reserved. +# +# For licensing information see the LICENSE.md file in the project root. +#++ + require 'coveralls' Coveralls.wear!