Source code for SoftLayer.managers.hardware

"""
    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']


[docs]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
[docs] 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
[docs] @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)
[docs] @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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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', }
[docs] @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]
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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
[docs] 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)
[docs] 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
[docs] 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_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)