text: """
      SoftLayer.hardware
      ~~~~~~~~~~~~~~~~~~
      Hardware Manager/helpers
  
      :license: MIT, see LICENSE for more details.
  """
  import logging
  import socket
  import time
  
  import SoftLayer
  from SoftLayer.decoration import retry
  from SoftLayer.managers import ordering
  from SoftLayer import utils
  
  LOGGER = logging.getLogger(__name__)
  
  # Invalid names are ignored due to long method names and short argument names
  # pylint: disable=invalid-name, no-self-use
  
  EXTRA_CATEGORIES = ['pri_ipv6_addresses',
                      'static_ipv6_addresses',
                      'sec_ip_addresses']
  
  
  class HardwareManager(utils.IdentifierMixin, object):
      """Manage SoftLayer hardware servers.
  
      Example::
  
         # Initialize the Manager.
         # env variables. These can also be specified in ~/.softlayer,
         # or passed directly to SoftLayer.Client()
         # SL_USERNAME = YOUR_USERNAME
         # SL_API_KEY = YOUR_API_KEY
         import SoftLayer
         client = SoftLayer.Client()
         mgr = SoftLayer.HardwareManager(client)
  
      See product information here: http://www.softlayer.com/bare-metal-servers
  
      :param SoftLayer.API.BaseClient client: the client instance
      :param SoftLayer.managers.OrderingManager ordering_manager: an optional
                                                manager to handle ordering.
                                                If none is provided, one will be
                                                auto initialized.
      """
  
      def __init__(self, client, ordering_manager=None):
          self.client = client
          self.hardware = self.client['Hardware_Server']
          self.account = self.client['Account']
          self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname]
          if ordering_manager is None:
              self.ordering_manager = ordering.OrderingManager(client)
          else:
              self.ordering_manager = ordering_manager
  
      def cancel_hardware(self, hardware_id, reason='unneeded', comment='', immediate=False):
          """Cancels the specified dedicated server.
  
          Example::
  
              # Cancels hardware id 1234
              result = mgr.cancel_hardware(hardware_id=1234)
  
          :param int hardware_id: The ID of the hardware to be cancelled.
          :param string reason: The reason code for the cancellation. This should come from
                                :func:`get_cancellation_reasons`.
          :param string comment: An optional comment to include with the cancellation.
          :param bool immediate: If set to True, will automatically update the cancelation ticket to request
                                 the resource be reclaimed asap. This request still has to be reviewed by a human
          :returns: True on success or an exception
          """
  
          # Get cancel reason
          reasons = self.get_cancellation_reasons()
          cancel_reason = reasons.get(reason, reasons['unneeded'])
          ticket_mgr = SoftLayer.TicketManager(self.client)
          mask = 'mask[id, hourlyBillingFlag, billingItem[id], openCancellationTicket[id], activeTransaction]'
          hw_billing = self.get_hardware(hardware_id, mask=mask)
  
          if 'activeTransaction' in hw_billing:
              raise SoftLayer.SoftLayerError("Unable to cancel hardware with running transaction")
  
          if 'billingItem' not in hw_billing:
              if utils.lookup(hw_billing, 'openCancellationTicket', 'id'):
                  raise SoftLayer.SoftLayerError("Ticket #%s already exists for this server" %
                                                 hw_billing['openCancellationTicket']['id'])
              raise SoftLayer.SoftLayerError("Cannot locate billing for the server. "
                                             "The server may already be cancelled.")
  
          billing_id = hw_billing['billingItem']['id']
  
          if immediate and not hw_billing['hourlyBillingFlag']:
              LOGGER.warning("Immediate cancellation of monthly servers is not guaranteed."
                             "Please check the cancellation ticket for updates.")
  
              result = self.client.call('Billing_Item', 'cancelItem',
                                        False, False, cancel_reason, comment, id=billing_id)
              hw_billing = self.get_hardware(hardware_id, mask=mask)
              ticket_number = hw_billing['openCancellationTicket']['id']
              cancel_message = "Please reclaim this server ASAP, it is no longer needed. Thankyou."
              ticket_mgr.update_ticket(ticket_number, cancel_message)
              LOGGER.info("Cancelation ticket #%s has been updated requesting immediate reclaim", ticket_number)
          else:
              result = self.client.call('Billing_Item', 'cancelItem',
                                        immediate, False, cancel_reason, comment, id=billing_id)
              hw_billing = self.get_hardware(hardware_id, mask=mask)
              ticket_number = hw_billing['openCancellationTicket']['id']
              LOGGER.info("Cancelation ticket #%s has been created", ticket_number)
  
          return result
  
      @retry(logger=LOGGER)
      def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None,
                        domain=None, datacenter=None, nic_speed=None,
                        public_ip=None, private_ip=None, **kwargs):
          """List all hardware (servers and bare metal computing instances).
  
          :param list tags: filter based on tags
          :param integer cpus: filter based on number of CPUS
          :param integer memory: filter based on amount of memory in gigabytes
          :param string hostname: filter based on hostname
          :param string domain: filter based on domain
          :param string datacenter: filter based on datacenter
          :param integer nic_speed: filter based on network speed (in MBPS)
          :param string public_ip: filter based on public ip address
          :param string private_ip: filter based on private ip address
          :param dict \\*\\*kwargs: response-level options (mask, limit, etc.)
          :returns: Returns a list of dictionaries representing the matching
                    hardware. This list will contain both dedicated servers and
                    bare metal computing instances
  
         Example::
  
              # Using a custom object-mask. Will get ONLY what is specified
              # These will stem from the SoftLayer_Hardware_Server datatype
              object_mask = "mask[hostname,monitoringRobot[robotStatus]]"
              result = mgr.list_hardware(mask=object_mask)
          """
          if 'mask' not in kwargs:
              hw_items = [
                  'id',
                  'hostname',
                  'domain',
                  'hardwareStatusId',
                  'globalIdentifier',
                  'fullyQualifiedDomainName',
                  'processorPhysicalCoreAmount',
                  'memoryCapacity',
                  'primaryBackendIpAddress',
                  'primaryIpAddress',
                  'datacenter',
              ]
              server_items = [
                  'activeTransaction[id, transactionStatus[friendlyName,name]]',
              ]
  
              kwargs['mask'] = ('[mask[%s],'
                                ' mask(SoftLayer_Hardware_Server)[%s]]'
                                % (','.join(hw_items), ','.join(server_items)))
  
          _filter = utils.NestedDict(kwargs.get('filter') or {})
          if tags:
              _filter['hardware']['tagReferences']['tag']['name'] = {
                  'operation': 'in',
                  'options': [{'name': 'data', 'value': tags}],
              }
  
          if cpus:
              _filter['hardware']['processorPhysicalCoreAmount'] = (
                  utils.query_filter(cpus))
  
          if memory:
              _filter['hardware']['memoryCapacity'] = utils.query_filter(memory)
  
          if hostname:
              _filter['hardware']['hostname'] = utils.query_filter(hostname)
  
          if domain:
              _filter['hardware']['domain'] = utils.query_filter(domain)
  
          if datacenter:
              _filter['hardware']['datacenter']['name'] = (
                  utils.query_filter(datacenter))
  
          if nic_speed:
              _filter['hardware']['networkComponents']['maxSpeed'] = (
                  utils.query_filter(nic_speed))
  
          if public_ip:
              _filter['hardware']['primaryIpAddress'] = (
                  utils.query_filter(public_ip))
  
          if private_ip:
              _filter['hardware']['primaryBackendIpAddress'] = (
                  utils.query_filter(private_ip))
  
          kwargs['filter'] = _filter.to_dict()
          kwargs['iter'] = True
          return self.client.call('Account', 'getHardware', **kwargs)
  
      @retry(logger=LOGGER)
      def get_hardware(self, hardware_id, **kwargs):
          """Get details about a hardware device.
  
          :param integer id: the hardware ID
          :returns: A dictionary containing a large amount of information about
                    the specified server.
  
          Example::
  
              object_mask = "mask[id,networkVlans[vlanNumber]]"
              # Object masks are optional
              result = mgr.get_hardware(hardware_id=1234,mask=object_mask)
          """
  
          if 'mask' not in kwargs:
              kwargs['mask'] = (
                  'id,'
                  'globalIdentifier,'
                  'fullyQualifiedDomainName,'
                  'hostname,'
                  'domain,'
                  'provisionDate,'
                  'hardwareStatus,'
                  'processorPhysicalCoreAmount,'
                  'memoryCapacity,'
                  'notes,'
                  'privateNetworkOnlyFlag,'
                  'primaryBackendIpAddress,'
                  'primaryIpAddress,'
                  'networkManagementIpAddress,'
                  'userData,'
                  'datacenter,'
                  '''networkComponents[id, status, speed, maxSpeed, name,
                     ipmiMacAddress, ipmiIpAddress, macAddress, primaryIpAddress,
                     port, primarySubnet[id, netmask, broadcastAddress,
                                         networkIdentifier, gateway]],'''
                  'hardwareChassis[id,name],'
                  'activeTransaction[id, transactionStatus[friendlyName,name]],'
                  '''operatingSystem[
                      softwareLicense[softwareDescription[manufacturer,
                                                          name,
                                                          version,
                                                          referenceCode]],
                      passwords[username,password]],'''
                  '''softwareComponents[
                      softwareLicense[softwareDescription[manufacturer,
                                                          name,
                                                          version,
                                                          referenceCode]],
                      passwords[username,password]],'''
                  'billingItem['
                  'id,nextInvoiceTotalRecurringAmount,'
                  'children[nextInvoiceTotalRecurringAmount],'
                  'orderItem.order.userRecord[username]'
                  '],'
                  'hourlyBillingFlag,'
                  'tagReferences[id,tag[name,id]],'
                  'networkVlans[id,vlanNumber,networkSpace],'
                  'remoteManagementAccounts[username,password]'
              )
  
          return self.hardware.getObject(id=hardware_id, **kwargs)
  
      def reload(self, hardware_id, post_uri=None, ssh_keys=None):
          """Perform an OS reload of a server with its current configuration.
  
          :param integer hardware_id: the instance ID to reload
          :param string post_uri: The URI of the post-install script to run
                                  after reload
          :param list ssh_keys: The SSH keys to add to the root user
          """
  
          config = {}
  
          if post_uri:
              config['customProvisionScriptUri'] = post_uri
  
          if ssh_keys:
              config['sshKeyIds'] = list(ssh_keys)
  
          return self.hardware.reloadOperatingSystem('FORCE', config,
                                                     id=hardware_id)
  
      def rescue(self, hardware_id):
          """Reboot a server into the a recsue kernel.
  
          :param integer instance_id: the server ID to rescue
  
          Example::
  
              result = mgr.rescue(1234)
          """
          return self.hardware.bootToRescueLayer(id=hardware_id)
  
      def change_port_speed(self, hardware_id, public, speed):
          """Allows you to change the port speed of a server's NICs.
  
          :param int hardware_id: The ID of the server
          :param bool public: Flag to indicate which interface to change.
                              True (default) means the public interface.
                              False indicates the private interface.
          :param int speed: The port speed to set.
  
          .. warning::
              A port speed of 0 will disable the interface.
  
          Example::
  
              #change the Public interface to 10Mbps on instance 12345
              result = mgr.change_port_speed(hardware_id=12345,
                                             public=True, speed=10)
              # result will be True or an Exception
          """
          if public:
              return self.client.call('Hardware_Server',
                                      'setPublicNetworkInterfaceSpeed',
                                      speed, id=hardware_id)
          else:
              return self.client.call('Hardware_Server',
                                      'setPrivateNetworkInterfaceSpeed',
                                      speed, id=hardware_id)
  
      def place_order(self, **kwargs):
          """Places an order for a piece of hardware.
  
          See get_create_options() for valid arguments.
  
          :param string size: server size name or presetId
          :param string hostname: server hostname
          :param string domain: server domain name
          :param string location: location (datacenter) name
          :param string os: operating system name
          :param int port_speed: Port speed in Mbps
          :param list ssh_keys: list of ssh key ids
          :param string post_uri: The URI of the post-install script to run
                                  after reload
          :param boolean hourly: True if using hourly pricing (default).
                                 False for monthly.
          :param boolean no_public: True if this server should only have private
                                    interfaces
          :param list extras: List of extra feature names
          """
          create_options = self._generate_create_dict(**kwargs)
          return self.client['Product_Order'].placeOrder(create_options)
  
      def verify_order(self, **kwargs):
          """Verifies an order for a piece of hardware.
  
          See :func:`place_order` for a list of available options.
          """
          create_options = self._generate_create_dict(**kwargs)
          return self.client['Product_Order'].verifyOrder(create_options)
  
      def get_cancellation_reasons(self):
          """Returns a dictionary of valid cancellation reasons.
  
          These can be used when cancelling a dedicated server
          via :func:`cancel_hardware`.
          """
          return {
              'unneeded': 'No longer needed',
              'closing': 'Business closing down',
              'cost': 'Server / Upgrade Costs',
              'migrate_larger': 'Migrating to larger server',
              'migrate_smaller': 'Migrating to smaller server',
              'datacenter': 'Migrating to a different SoftLayer datacenter',
              'performance': 'Network performance / latency',
              'support': 'Support response / timing',
              'sales': 'Sales process / upgrades',
              'moving': 'Moving to competitor',
          }
  
      @retry(logger=LOGGER)
      def get_create_options(self):
          """Returns valid options for ordering hardware."""
  
          package = self._get_package()
  
          # Locations
          locations = []
          for region in package['regions']:
              locations.append({
                  'name': region['location']['location']['longName'],
                  'key': region['location']['location']['name'],
              })
  
          # Sizes
          sizes = []
          for preset in package['activePresets'] + package['accountRestrictedActivePresets']:
              sizes.append({
                  'name': preset['description'],
                  'key': preset['keyName']
              })
  
          # Operating systems
          operating_systems = []
          for item in package['items']:
              if item['itemCategory']['categoryCode'] == 'os':
                  operating_systems.append({
                      'name': item['softwareDescription']['longDescription'],
                      'key': item['keyName']
                  })
  
          # Port speeds
          port_speeds = []
          for item in package['items']:
              if all([item['itemCategory']['categoryCode'] == 'port_speed',
                      # Hide private options
                      not _is_private_port_speed_item(item),
                      # Hide unbonded options
                      _is_bonded(item)]):
                  port_speeds.append({
                      'name': item['description'],
                      'key': item['capacity'],
                  })
  
          # Extras
          extras = []
          for item in package['items']:
              if item['itemCategory']['categoryCode'] in EXTRA_CATEGORIES:
                  extras.append({
                      'name': item['description'],
                      'key': item['keyName']
                  })
  
          return {
              'locations': locations,
              'sizes': sizes,
              'operating_systems': operating_systems,
              'port_speeds': port_speeds,
              'extras': extras,
          }
  
      @retry(logger=LOGGER)
      def _get_package(self):
          """Get the package related to simple hardware ordering."""
          mask = '''
              items[
                  keyName,
                  capacity,
                  description,
                  attributes[id,attributeTypeKeyName],
                  itemCategory[id,categoryCode],
                  softwareDescription[id,referenceCode,longDescription],
                  prices
              ],
              activePresets,
              accountRestrictedActivePresets,
              regions[location[location[priceGroups]]]
              '''
  
          package_keyname = 'BARE_METAL_SERVER'
          package = self.ordering_manager.get_package_by_key(package_keyname, mask=mask)
          return package
  
      def _generate_create_dict(self,
                                size=None,
                                hostname=None,
                                domain=None,
                                location=None,
                                os=None,
                                port_speed=None,
                                ssh_keys=None,
                                post_uri=None,
                                hourly=True,
                                no_public=False,
                                extras=None):
          """Translates arguments into a dictionary for creating a server."""
  
          extras = extras or []
  
          package = self._get_package()
          location = _get_location(package, location)
  
          prices = []
          for category in ['pri_ip_addresses',
                           'vpn_management',
                           'remote_management']:
              prices.append(_get_default_price_id(package['items'],
                                                  option=category,
                                                  hourly=hourly,
                                                  location=location))
  
          prices.append(_get_os_price_id(package['items'], os,
                                         location=location))
          prices.append(_get_bandwidth_price_id(package['items'],
                                                hourly=hourly,
                                                no_public=no_public,
                                                location=location))
          prices.append(_get_port_speed_price_id(package['items'],
                                                 port_speed,
                                                 no_public,
                                                 location=location))
  
          for extra in extras:
              prices.append(_get_extra_price_id(package['items'],
                                                extra, hourly,
                                                location=location))
  
          hardware = {
              'hostname': hostname,
              'domain': domain,
          }
  
          order = {
              'hardware': [hardware],
              'location': location['keyname'],
              'prices': [{'id': price} for price in prices],
              'packageId': package['id'],
              'presetId': _get_preset_id(package, size),
              'useHourlyPricing': hourly,
          }
  
          if post_uri:
              order['provisionScripts'] = [post_uri]
  
          if ssh_keys:
              order['sshKeys'] = [{'sshKeyIds': ssh_keys}]
  
          return order
  
      def _get_ids_from_hostname(self, hostname):
          """Returns list of matching hardware IDs for a given hostname."""
          results = self.list_hardware(hostname=hostname, mask="id")
          return [result['id'] for result in results]
  
      def _get_ids_from_ip(self, ip):  # pylint: disable=inconsistent-return-statements
          """Returns list of matching hardware IDs for a given ip address."""
          try:
              # Does it look like an ip address?
              socket.inet_aton(ip)
          except socket.error:
              return []
  
          # Find the server via ip address. First try public ip, then private
          results = self.list_hardware(public_ip=ip, mask="id")
          if results:
              return [result['id'] for result in results]
  
          results = self.list_hardware(private_ip=ip, mask="id")
          if results:
              return [result['id'] for result in results]
  
      def edit(self, hardware_id, userdata=None, hostname=None, domain=None,
               notes=None, tags=None):
          """Edit hostname, domain name, notes, user data of the hardware.
  
          Parameters set to None will be ignored and not attempted to be updated.
  
          :param integer hardware_id: the instance ID to edit
          :param string userdata: user data on the hardware to edit.
                                  If none exist it will be created
          :param string hostname: valid hostname
          :param string domain: valid domain name
          :param string notes: notes about this particular hardware
          :param string tags: tags to set on the hardware as a comma separated
                              list. Use the empty string to remove all tags.
  
          Example::
  
              # Change the hostname on instance 12345 to 'something'
              result = mgr.edit(hardware_id=12345 , hostname="something")
              #result will be True or an Exception
          """
  
          obj = {}
          if userdata:
              self.hardware.setUserMetadata([userdata], id=hardware_id)
  
          if tags is not None:
              self.hardware.setTags(tags, id=hardware_id)
  
          if hostname:
              obj['hostname'] = hostname
  
          if domain:
              obj['domain'] = domain
  
          if notes:
              obj['notes'] = notes
  
          if not obj:
              return True
  
          return self.hardware.editObject(obj, id=hardware_id)
  
      def update_firmware(self,
                          hardware_id,
                          ipmi=True,
                          raid_controller=True,
                          bios=True,
                          hard_drive=True):
          """Update hardware firmware.
  
          This will cause the server to be unavailable for ~20 minutes.
  
          :param int hardware_id: The ID of the hardware to have its firmware
                                  updated.
          :param bool ipmi: Update the ipmi firmware.
          :param bool raid_controller: Update the raid controller firmware.
          :param bool bios: Update the bios firmware.
          :param bool hard_drive: Update the hard drive firmware.
  
          Example::
  
              # Check the servers active transactions to see progress
              result = mgr.update_firmware(hardware_id=1234)
          """
  
          return self.hardware.createFirmwareUpdateTransaction(
              bool(ipmi), bool(raid_controller), bool(bios), bool(hard_drive), id=hardware_id)
  
      def reflash_firmware(self,
                           hardware_id,
                           ipmi=True,
                           raid_controller=True,
                           bios=True):
          """Reflash hardware firmware.
  
          This will cause the server to be unavailable for ~60 minutes.
          The firmware will not be upgraded but rather reflashed to the version installed.
  
          :param int hardware_id: The ID of the hardware to have its firmware
                                  reflashed.
          :param bool ipmi: Reflash the ipmi firmware.
          :param bool raid_controller: Reflash the raid controller firmware.
          :param bool bios: Reflash the bios firmware.
  
          Example::
  
              # Check the servers active transactions to see progress
              result = mgr.reflash_firmware(hardware_id=1234)
          """
  
          return self.hardware.createFirmwareReflashTransaction(
              bool(ipmi), bool(raid_controller), bool(bios), id=hardware_id)
  
      def wait_for_ready(self, instance_id, limit=14400, delay=10, pending=False):
          """Determine if a Server is ready.
  
          A server is ready when no transactions are running on it.
  
          :param int instance_id: The instance ID with the pending transaction
          :param int limit: The maximum amount of seconds to wait.
          :param int delay: The number of seconds to sleep before checks. Defaults to 10.
          """
          now = time.time()
          until = now + limit
          mask = "mask[id, lastOperatingSystemReload[id], activeTransaction, provisionDate]"
          instance = self.get_hardware(instance_id, mask=mask)
          while now <= until:
              if utils.is_ready(instance, pending):
                  return True
              transaction = utils.lookup(instance, 'activeTransaction', 'transactionStatus', 'friendlyName')
              snooze = min(delay, until - now)
              LOGGER.info("%s - %d not ready. Auto retry in %ds", transaction, instance_id, snooze)
              time.sleep(snooze)
              instance = self.get_hardware(instance_id, mask=mask)
              now = time.time()
  
          LOGGER.info("Waiting for %d expired.", instance_id)
          return False
  
      def get_tracking_id(self, instance_id):
          """Returns the Metric Tracking Object Id for a hardware server
  
          :param int instance_id: Id of the hardware server
          """
          return self.hardware.getMetricTrackingObjectId(id=instance_id)
  
      def get_bandwidth_data(self, instance_id, start_date=None, end_date=None, direction=None, rollup=3600):
          """Gets bandwidth data for a server
  
          Will get averaged bandwidth data for a given time period. If you use a rollup over 3600 be aware
          that the API will bump your start/end date to align with how data is stored. For example if you
          have a rollup of 86400 your start_date will be bumped to 00:00. If you are not using a time in the
          start/end date fields, this won't really matter.
  
          :param int instance_id: Hardware Id to get data for
          :param date start_date: Date to start pulling data for.
          :param date end_date: Date to finish pulling data for
          :param string direction: Can be either 'public', 'private', or None for both.
          :param int rollup: 300, 600, 1800, 3600, 43200 or 86400 seconds to average data over.
          """
          tracking_id = self.get_tracking_id(instance_id)
          data = self.client.call('Metric_Tracking_Object', 'getBandwidthData', start_date, end_date, direction,
                                  rollup, id=tracking_id, iter=True)
          return data
  
      def get_bandwidth_allocation(self, instance_id):
          """Combines getBandwidthAllotmentDetail() and getBillingCycleBandwidthUsage() """
          a_mask = "mask[allocation[amount]]"
          allotment = self.client.call('Hardware_Server', 'getBandwidthAllotmentDetail', id=instance_id, mask=a_mask)
          u_mask = "mask[amountIn,amountOut,type]"
          usage = self.client.call('Hardware_Server', 'getBillingCycleBandwidthUsage', id=instance_id, mask=u_mask)
          if allotment:
              return {'allotment': allotment.get('allocation'), 'usage': usage}
          return {'allotment': allotment, 'usage': usage}
  
      def get_storage_details(self, instance_id, nas_type):
          """Returns the hardware server attached network storage.
  
          :param int instance_id: Id of the hardware server
          :param nas_type: storage type.
          """
          mask = 'mask[id,username,capacityGb,notes,serviceResourceBackendIpAddress,' \
                 'allowedHardware[id,datacenter]]'
          return self.hardware.getAttachedNetworkStorages(nas_type, mask=mask, id=instance_id)
  
      def get_storage_credentials(self, instance_id):
          """Returns the hardware server storage credentials.
  
          :param int instance_id: Id of the hardware server
          """
          mask = 'mask[credential]'
          return self.hardware.getAllowedHost(mask=mask, id=instance_id)
  
      def get_hard_drives(self, instance_id):
          """Returns the hardware server hard drives.
  
          :param int instance_id: Id of the hardware server
          """
          return self.hardware.getHardDrives(id=instance_id)
  
  
  def _get_extra_price_id(items, key_name, hourly, location):
      """Returns a price id attached to item with the given key_name."""
  
      for item in items:
          if utils.lookup(item, 'keyName') != key_name:
              continue
  
          for price in item['prices']:
              if not _matches_billing(price, hourly):
                  continue
  
              if not _matches_location(price, location):
                  continue
  
              return price['id']
  
      raise SoftLayer.SoftLayerError(
          "Could not find valid price for extra option, '%s'" % key_name)
  
  
  def _get_default_price_id(items, option, hourly, location):
      """Returns a 'free' price id given an option."""
  
      for item in items:
          if utils.lookup(item, 'itemCategory', 'categoryCode') != option:
              continue
  
          for price in item['prices']:
              if all([float(price.get('hourlyRecurringFee', 0)) == 0.0,
                      float(price.get('recurringFee', 0)) == 0.0,
                      _matches_billing(price, hourly),
                      _matches_location(price, location)]):
                  return price['id']
  
      raise SoftLayer.SoftLayerError(
          "Could not find valid price for '%s' option" % option)
  
  
  def _get_bandwidth_price_id(items,
                              hourly=True,
                              no_public=False,
                              location=None):
      """Choose a valid price id for bandwidth."""
  
      # Prefer pay-for-use data transfer with hourly
      for item in items:
  
          capacity = float(item.get('capacity', 0))
          # Hourly and private only do pay-as-you-go bandwidth
          if any([utils.lookup(item,
                               'itemCategory',
                               'categoryCode') != 'bandwidth',
                  (hourly or no_public) and capacity != 0.0,
                  not (hourly or no_public) and capacity == 0.0]):
              continue
  
          for price in item['prices']:
              if not _matches_billing(price, hourly):
                  continue
              if not _matches_location(price, location):
                  continue
  
              return price['id']
  
      raise SoftLayer.SoftLayerError(
          "Could not find valid price for bandwidth option")
  
  
  def _get_os_price_id(items, os, location):
      """Returns the price id matching."""
  
      for item in items:
          if any([utils.lookup(item,
                               'itemCategory',
                               'categoryCode') != 'os',
                  utils.lookup(item,
                               'keyName') != os]):
              continue
  
          for price in item['prices']:
              if not _matches_location(price, location):
                  continue
  
              return price['id']
  
      raise SoftLayer.SoftLayerError("Could not find valid price for os: '%s'" %
                                     os)
  
  
  def _get_port_speed_price_id(items, port_speed, no_public, location):
      """Choose a valid price id for port speed."""
  
      for item in items:
          if utils.lookup(item,
                          'itemCategory',
                          'categoryCode') != 'port_speed':
              continue
  
          # Check for correct capacity and if the item matches private only
          if any([int(utils.lookup(item, 'capacity')) != port_speed,
                  _is_private_port_speed_item(item) != no_public,
                  not _is_bonded(item)]):
              continue
  
          for price in item['prices']:
              if not _matches_location(price, location):
                  continue
  
              return price['id']
  
      raise SoftLayer.SoftLayerError(
          "Could not find valid price for port speed: '%s'" % port_speed)
  
  
  def _matches_billing(price, hourly):
      """Return True if the price object is hourly and/or monthly."""
      return any([hourly and price.get('hourlyRecurringFee') is not None,
                  not hourly and price.get('recurringFee') is not None])
  
  
  def _matches_location(price, location):
      """Return True if the price object matches the location."""
      # the price has no location restriction
      if not price.get('locationGroupId'):
          return True
  
      # Check to see if any of the location groups match the location group
      # of this price object
      for group in location['location']['location']['priceGroups']:
          if group['id'] == price['locationGroupId']:
              return True
  
      return False
  
  
  def _is_private_port_speed_item(item):
      """Determine if the port speed item is private network only."""
      for attribute in item['attributes']:
          if attribute['attributeTypeKeyName'] == 'IS_PRIVATE_NETWORK_ONLY':
              return True
  
      return False
  
  
  def _is_bonded(item):
      """Determine if the item refers to a bonded port."""
      for attribute in item['attributes']:
          if attribute['attributeTypeKeyName'] == 'NON_LACP':
              return False
  
      return True
  
  
  def _get_location(package, location):
      """Get the longer key with a short location name."""
      for region in package['regions']:
          if region['location']['location']['name'] == location:
              return region
  
      raise SoftLayer.SoftLayerError("Could not find valid location for: '%s'" % location)
  
  
  def _get_preset_id(package, size):
      """Get the preset id given the keyName of the preset."""
      for preset in package['activePresets'] + package['accountRestrictedActivePresets']:
          if preset['keyName'] == size or preset['id'] == size:
              return preset['id']
  
      raise SoftLayer.SoftLayerError("Could not find valid size for: '%s'" % size)
  