Skip to content

Commit 179c8ae

Browse files
Merge pull request #917 from allmightyspiff/issues588
Issues588 - hardware wait for ready
2 parents a1e984a + ed18023 commit 179c8ae

File tree

16 files changed

+319
-152
lines changed

16 files changed

+319
-152
lines changed

SoftLayer/CLI/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
:license: MIT, see LICENSE for more details.
77
"""
88
# pylint: disable=w0401, invalid-name
9+
import logging
910

1011
from SoftLayer.CLI.helpers import * # NOQA
12+
13+
logger = logging.getLogger()
14+
logger.addHandler(logging.StreamHandler())
15+
logger.setLevel(logging.INFO)

SoftLayer/CLI/hardware/ready.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Check if a virtual server is ready."""
2+
# :license: MIT, see LICENSE for more details.
3+
4+
import click
5+
6+
import SoftLayer
7+
from SoftLayer.CLI import environment
8+
from SoftLayer.CLI import exceptions
9+
from SoftLayer.CLI import helpers
10+
11+
12+
@click.command()
13+
@click.argument('identifier')
14+
@click.option('--wait', default=0, show_default=True, type=click.INT, help="Seconds to wait")
15+
@environment.pass_env
16+
def cli(env, identifier, wait):
17+
"""Check if a virtual server is ready."""
18+
19+
compute = SoftLayer.HardwareManager(env.client)
20+
compute_id = helpers.resolve_id(compute.resolve_ids, identifier, 'hardware')
21+
ready = compute.wait_for_ready(compute_id, wait)
22+
if ready:
23+
env.fout("READY")
24+
else:
25+
raise exceptions.CLIAbort("Instance %s not ready" % compute_id)

SoftLayer/CLI/routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
('hardware:credentials', 'SoftLayer.CLI.hardware.credentials:cli'),
231231
('hardware:update-firmware', 'SoftLayer.CLI.hardware.update_firmware:cli'),
232232
('hardware:rescue', 'SoftLayer.CLI.hardware.power:rescue'),
233+
('hardware:ready', 'SoftLayer.CLI.hardware.ready:cli'),
233234

234235
('securitygroup', 'SoftLayer.CLI.securitygroup'),
235236
('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'),

SoftLayer/CLI/virt/ready.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@
1111

1212
@click.command()
1313
@click.argument('identifier')
14-
@click.option('--wait',
15-
default=0,
16-
show_default=True,
17-
type=click.INT,
18-
help="Name of the image")
14+
@click.option('--wait', default=0, show_default=True, type=click.INT, help="Seconds to wait")
1915
@environment.pass_env
2016
def cli(env, identifier, wait):
2117
"""Check if a virtual server is ready."""

SoftLayer/decoration.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,17 @@
99
from random import randint
1010
from time import sleep
1111

12+
from SoftLayer import exceptions
1213

13-
def retry(ex, tries=4, delay=5, backoff=2, logger=None):
14+
RETRIABLE = (
15+
exceptions.ServerError,
16+
exceptions.ApplicationError,
17+
exceptions.RemoteSystemError,
18+
exceptions.TransportError
19+
)
20+
21+
22+
def retry(ex=RETRIABLE, tries=4, delay=5, backoff=2, logger=None):
1423
"""Retry calling the decorated function using an exponential backoff.
1524
1625
http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/

SoftLayer/exceptions.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,10 @@ def __init__(self, fault_code, fault_string, *args):
2727
self.reason = self.faultString = fault_string
2828

2929
def __repr__(self):
30-
return '<%s(%s): %s>' % (self.__class__.__name__,
31-
self.faultCode,
32-
self.faultString)
30+
return '<%s(%s): %s>' % (self.__class__.__name__, self.faultCode, self.faultString)
3331

3432
def __str__(self):
35-
return '%s(%s): %s' % (self.__class__.__name__,
36-
self.faultCode,
37-
self.faultString)
33+
return '%s(%s): %s' % (self.__class__.__name__, self.faultCode, self.faultString)
3834

3935

4036
class ParseError(SoftLayerAPIError):
@@ -78,12 +74,12 @@ class SpecViolation(ServerError):
7874
pass
7975

8076

81-
class MethodNotFound(ServerError):
77+
class MethodNotFound(SoftLayerAPIError):
8278
"""Method name not found."""
8379
pass
8480

8581

86-
class InvalidMethodParameters(ServerError):
82+
class InvalidMethodParameters(SoftLayerAPIError):
8783
"""Invalid method paramters."""
8884
pass
8985

SoftLayer/managers/hardware.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
"""
88
import logging
99
import socket
10+
import time
1011

1112
import SoftLayer
1213
from SoftLayer.decoration import retry
13-
from SoftLayer import exceptions
1414
from SoftLayer.managers import ordering
1515
from SoftLayer import utils
1616

@@ -88,7 +88,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='',
8888
immediate, False, cancel_reason, comment,
8989
id=billing_id)
9090

91-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
91+
@retry(logger=LOGGER)
9292
def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None,
9393
domain=None, datacenter=None, nic_speed=None,
9494
public_ip=None, private_ip=None, **kwargs):
@@ -176,7 +176,7 @@ def list_hardware(self, tags=None, cpus=None, memory=None, hostname=None,
176176
kwargs['filter'] = _filter.to_dict()
177177
return self.account.getHardware(**kwargs)
178178

179-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
179+
@retry(logger=LOGGER)
180180
def get_hardware(self, hardware_id, **kwargs):
181181
"""Get details about a hardware device.
182182
@@ -343,7 +343,7 @@ def get_cancellation_reasons(self):
343343
'moving': 'Moving to competitor',
344344
}
345345

346-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
346+
@retry(logger=LOGGER)
347347
def get_create_options(self):
348348
"""Returns valid options for ordering hardware."""
349349

@@ -404,7 +404,7 @@ def get_create_options(self):
404404
'extras': extras,
405405
}
406406

407-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
407+
@retry(logger=LOGGER)
408408
def _get_package(self):
409409
"""Get the package related to simple hardware ordering."""
410410
mask = '''
@@ -584,8 +584,33 @@ def update_firmware(self,
584584
"""
585585

586586
return self.hardware.createFirmwareUpdateTransaction(
587-
bool(ipmi), bool(raid_controller), bool(bios), bool(hard_drive),
588-
id=hardware_id)
587+
bool(ipmi), bool(raid_controller), bool(bios), bool(hard_drive), id=hardware_id)
588+
589+
def wait_for_ready(self, instance_id, limit=14400, delay=10, pending=False):
590+
"""Determine if a Server is ready.
591+
592+
A server is ready when no transactions are running on it.
593+
594+
:param int instance_id: The instance ID with the pending transaction
595+
:param int limit: The maximum amount of seconds to wait.
596+
:param int delay: The number of seconds to sleep before checks. Defaults to 10.
597+
"""
598+
now = time.time()
599+
until = now + limit
600+
mask = "mask[id, lastOperatingSystemReload[id], activeTransaction, provisionDate]"
601+
instance = self.get_hardware(instance_id, mask=mask)
602+
while now <= until:
603+
if utils.is_ready(instance, pending):
604+
return True
605+
transaction = utils.lookup(instance, 'activeTransaction', 'transactionStatus', 'friendlyName')
606+
snooze = min(delay, until - now)
607+
LOGGER.info("%s - %d not ready. Auto retry in %ds", transaction, instance_id, snooze)
608+
time.sleep(snooze)
609+
instance = self.get_hardware(instance_id, mask=mask)
610+
now = time.time()
611+
612+
LOGGER.info("Waiting for %d expired.", instance_id)
613+
return False
589614

590615

591616
def _get_extra_price_id(items, key_name, hourly, location):

SoftLayer/managers/vs.py

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
:license: MIT, see LICENSE for more details.
77
"""
88
import datetime
9-
import itertools
109
import logging
11-
import random
1210
import socket
1311
import time
1412
import warnings
@@ -58,7 +56,7 @@ def __init__(self, client, ordering_manager=None):
5856
else:
5957
self.ordering_manager = ordering_manager
6058

61-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
59+
@retry(logger=LOGGER)
6260
def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None,
6361
memory=None, hostname=None, domain=None,
6462
local_disk=None, datacenter=None, nic_speed=None,
@@ -162,7 +160,7 @@ def list_instances(self, hourly=True, monthly=True, tags=None, cpus=None,
162160
func = getattr(self.account, call)
163161
return func(**kwargs)
164162

165-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
163+
@retry(logger=LOGGER)
166164
def get_instance(self, instance_id, **kwargs):
167165
"""Get details about a virtual server instance.
168166
@@ -237,7 +235,7 @@ def get_instance(self, instance_id, **kwargs):
237235

238236
return self.guest.getObject(id=instance_id, **kwargs)
239237

240-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
238+
@retry(logger=LOGGER)
241239
def get_create_options(self):
242240
"""Retrieves the available options for creating a VS.
243241
@@ -414,7 +412,7 @@ def _generate_create_dict(
414412

415413
return data
416414

417-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
415+
@retry(logger=LOGGER)
418416
def wait_for_transaction(self, instance_id, limit, delay=10):
419417
"""Waits on a VS transaction for the specified amount of time.
420418
@@ -428,7 +426,7 @@ def wait_for_transaction(self, instance_id, limit, delay=10):
428426

429427
return self.wait_for_ready(instance_id, limit, delay=delay, pending=True)
430428

431-
def wait_for_ready(self, instance_id, limit, delay=10, pending=False):
429+
def wait_for_ready(self, instance_id, limit=3600, delay=10, pending=False):
432430
"""Determine if a VS is ready and available.
433431
434432
In some cases though, that can mean that no transactions are running.
@@ -439,7 +437,7 @@ def wait_for_ready(self, instance_id, limit, delay=10, pending=False):
439437
cancellations.
440438
441439
:param int instance_id: The instance ID with the pending transaction
442-
:param int limit: The maximum amount of time to wait.
440+
:param int limit: The maximum amount of seconds to wait.
443441
:param int delay: The number of seconds to sleep before checks. Defaults to 10.
444442
:param bool pending: Wait for pending transactions not related to
445443
provisioning or reloads such as monitoring.
@@ -449,43 +447,21 @@ def wait_for_ready(self, instance_id, limit, delay=10, pending=False):
449447
# Will return once vsi 12345 is ready, or after 10 checks
450448
ready = mgr.wait_for_ready(12345, 10)
451449
"""
452-
until = time.time() + limit
453-
for new_instance in itertools.repeat(instance_id):
454-
mask = """id,
455-
lastOperatingSystemReload.id,
456-
activeTransaction.id,provisionDate"""
457-
try:
458-
instance = self.get_instance(new_instance, mask=mask)
459-
last_reload = utils.lookup(instance, 'lastOperatingSystemReload', 'id')
460-
active_transaction = utils.lookup(instance, 'activeTransaction', 'id')
461-
462-
reloading = all((
463-
active_transaction,
464-
last_reload,
465-
last_reload == active_transaction,
466-
))
467-
468-
# only check for outstanding transactions if requested
469-
outstanding = False
470-
if pending:
471-
outstanding = active_transaction
472-
473-
# return True if the instance has finished provisioning
474-
# and isn't currently reloading the OS.
475-
if all([instance.get('provisionDate'),
476-
not reloading,
477-
not outstanding]):
478-
return True
479-
LOGGER.info("%s not ready.", str(instance_id))
480-
except exceptions.SoftLayerAPIError as exception:
481-
delay = (delay * 2) + random.randint(0, 9)
482-
LOGGER.info('Exception: %s', str(exception))
483-
450+
now = time.time()
451+
until = now + limit
452+
mask = "mask[id, lastOperatingSystemReload[id], activeTransaction, provisionDate]"
453+
454+
while now <= until:
455+
instance = self.get_instance(instance_id, mask=mask)
456+
if utils.is_ready(instance, pending):
457+
return True
458+
transaction = utils.lookup(instance, 'activeTransaction', 'transactionStatus', 'friendlyName')
459+
snooze = min(delay, until - now)
460+
LOGGER.info("%s - %d not ready. Auto retry in %ds", transaction, instance_id, snooze)
461+
time.sleep(snooze)
484462
now = time.time()
485-
if now >= until:
486-
return False
487-
LOGGER.info('Auto retry in %s seconds', str(min(delay, until - now)))
488-
time.sleep(min(delay, until - now))
463+
464+
LOGGER.info("Waiting for %d expired.", instance_id)
489465
return False
490466

491467
def verify_create_instance(self, **kwargs):
@@ -581,7 +557,7 @@ def create_instance(self, **kwargs):
581557
self.set_tags(tags, guest_id=inst['id'])
582558
return inst
583559

584-
@retry(exceptions.SoftLayerAPIError, logger=LOGGER)
560+
@retry(logger=LOGGER)
585561
def set_tags(self, tags, guest_id):
586562
"""Sets tags on a guest with a retry decorator
587563

SoftLayer/transports.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def __call__(self, request):
174174
verify = self.verify
175175

176176
LOGGER.debug("=== REQUEST ===")
177-
LOGGER.info('POST %s', url)
177+
LOGGER.debug('POST %s', url)
178178
LOGGER.debug(request.transport_headers)
179179
LOGGER.debug(payload)
180180

@@ -302,7 +302,7 @@ def __call__(self, request):
302302
verify = self.verify
303303

304304
LOGGER.debug("=== REQUEST ===")
305-
LOGGER.info(url)
305+
LOGGER.debug(url)
306306
LOGGER.debug(request.transport_headers)
307307
LOGGER.debug(raw_body)
308308
try:

SoftLayer/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,27 @@ def tzname(self, _):
185185

186186
def dst(self, _):
187187
return datetime.timedelta(0)
188+
189+
190+
def is_ready(instance, pending=False):
191+
"""Returns True if instance is ready to be used
192+
193+
:param Object instance: Hardware or Virt with transaction data retrieved from the API
194+
:param bool pending: Wait for ALL transactions to finish?
195+
:returns bool:
196+
"""
197+
198+
last_reload = lookup(instance, 'lastOperatingSystemReload', 'id')
199+
active_transaction = lookup(instance, 'activeTransaction', 'id')
200+
201+
reloading = all((
202+
active_transaction,
203+
last_reload,
204+
last_reload == active_transaction,
205+
))
206+
outstanding = False
207+
if pending:
208+
outstanding = active_transaction
209+
if instance.get('provisionDate') and not reloading and not outstanding:
210+
return True
211+
return False

0 commit comments

Comments
 (0)