Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/bioblend/cloudman/__init__.py @ 0:d30785e31577 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
| author | guerler |
|---|---|
| date | Fri, 31 Jul 2020 00:18:57 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:d30785e31577 |
|---|---|
| 1 """ | |
| 2 API for interacting with a CloudMan instance. | |
| 3 """ | |
| 4 import functools | |
| 5 import json | |
| 6 import time | |
| 7 from urllib.parse import urlparse | |
| 8 | |
| 9 import requests | |
| 10 | |
| 11 import bioblend | |
| 12 from bioblend.cloudman.launch import CloudManLauncher | |
| 13 from bioblend.util import Bunch | |
| 14 | |
| 15 | |
| 16 def block_until_vm_ready(func): | |
| 17 """ | |
| 18 This decorator exists to make sure that a launched VM is | |
| 19 ready and has received a public IP before allowing the wrapped | |
| 20 function call to continue. If the VM is not ready, the function will | |
| 21 block until the VM is ready. If the VM does not become ready | |
| 22 until the vm_ready_timeout elapses or the VM status returns an error, | |
| 23 a VMLaunchException will be thrown. | |
| 24 | |
| 25 This decorator relies on the wait_until_instance_ready method defined in | |
| 26 class GenericVMInstance. All methods to which this decorator is applied | |
| 27 must be members of a class which inherit from GenericVMInstance. | |
| 28 | |
| 29 The following two optional keyword arguments are recognized by this decorator: | |
| 30 | |
| 31 :type vm_ready_timeout: int | |
| 32 :param vm_ready_timeout: Maximum length of time to block before timing out. | |
| 33 Once the timeout is reached, a VMLaunchException | |
| 34 will be thrown. | |
| 35 | |
| 36 :type vm_ready_check_interval: int | |
| 37 :param vm_ready_check_interval: The number of seconds to pause between consecutive | |
| 38 calls when polling the VM's ready status. | |
| 39 """ | |
| 40 @functools.wraps(func) | |
| 41 def wrapper(*args, **kwargs): | |
| 42 obj = args[0] | |
| 43 timeout = kwargs.pop('vm_ready_timeout', 300) | |
| 44 interval = kwargs.pop('vm_ready_check_interval', 10) | |
| 45 try: | |
| 46 obj.wait_until_instance_ready(timeout, interval) | |
| 47 except AttributeError: | |
| 48 raise VMLaunchException("Decorated object does not define a wait_until_instance_ready method." | |
| 49 "Make sure that the object is of type GenericVMInstance.") | |
| 50 return func(*args, **kwargs) | |
| 51 return wrapper | |
| 52 | |
| 53 | |
| 54 class VMLaunchException(Exception): | |
| 55 def __init__(self, value): | |
| 56 self.value = value | |
| 57 | |
| 58 def __str__(self): | |
| 59 return repr(self.value) | |
| 60 | |
| 61 | |
| 62 class CloudManConfig(object): | |
| 63 | |
| 64 def __init__(self, | |
| 65 access_key=None, | |
| 66 secret_key=None, | |
| 67 cluster_name=None, | |
| 68 image_id=None, | |
| 69 instance_type='m1.medium', | |
| 70 password=None, | |
| 71 cloud_metadata=None, | |
| 72 cluster_type=None, | |
| 73 galaxy_data_option='', | |
| 74 initial_storage_size=10, | |
| 75 key_name='cloudman_key_pair', | |
| 76 security_groups=None, | |
| 77 placement='', | |
| 78 kernel_id=None, | |
| 79 ramdisk_id=None, | |
| 80 block_until_ready=False, | |
| 81 **kwargs): | |
| 82 """ | |
| 83 Initializes a CloudMan launch configuration object. | |
| 84 | |
| 85 :type access_key: str | |
| 86 :param access_key: Access credentials. | |
| 87 | |
| 88 :type secret_key: str | |
| 89 :param secret_key: Access credentials. | |
| 90 | |
| 91 :type cluster_name: str | |
| 92 :param cluster_name: Name used to identify this CloudMan cluster. | |
| 93 | |
| 94 :type image_id: str | |
| 95 :param image_id: Machine image ID to use when launching this | |
| 96 CloudMan instance. | |
| 97 | |
| 98 :type instance_type: str | |
| 99 :param instance_type: The type of the machine instance, as understood by | |
| 100 the chosen cloud provider. (e.g., ``m1.medium``) | |
| 101 | |
| 102 :type password: str | |
| 103 :param password: The administrative password for this CloudMan instance. | |
| 104 | |
| 105 :type cloud_metadata: Bunch | |
| 106 :param cloud_metadata: This object must define the properties required | |
| 107 to establish a `boto <https://github.com/boto/boto/>`_ | |
| 108 connection to that cloud. See this method's implementation | |
| 109 for an example of the required fields. Note that as | |
| 110 long the as provided object defines the required fields, | |
| 111 it can really by implemented as anything (e.g., | |
| 112 a Bunch, a database object, a custom class). If no | |
| 113 value for the ``cloud`` argument is provided, the | |
| 114 default is to use the Amazon cloud. | |
| 115 | |
| 116 :type kernel_id: str | |
| 117 :param kernel_id: The ID of the kernel with which to launch the | |
| 118 instances | |
| 119 | |
| 120 :type ramdisk_id: str | |
| 121 :param ramdisk_id: The ID of the RAM disk with which to launch the | |
| 122 instances | |
| 123 | |
| 124 :type key_name: str | |
| 125 :param key_name: The name of the key pair with which to launch instances | |
| 126 | |
| 127 :type security_groups: list of str | |
| 128 :param security_groups: The IDs of the security groups with which to | |
| 129 associate instances | |
| 130 | |
| 131 :type placement: str | |
| 132 :param placement: The availability zone in which to launch the instances | |
| 133 | |
| 134 :type cluster_type: str | |
| 135 :param cluster_type: The ``type``, either 'Galaxy', 'Data', or | |
| 136 'Test', defines the type of cluster platform to initialize. | |
| 137 | |
| 138 :type galaxy_data_option: str | |
| 139 :param galaxy_data_option: The storage type to use for this instance. | |
| 140 May be 'transient', 'custom_size' or ''. The default is '', | |
| 141 which will result in ignoring the bioblend specified | |
| 142 initial_storage_size. 'custom_size' must be used for | |
| 143 initial_storage_size to come into effect. | |
| 144 | |
| 145 :type initial_storage_size: int | |
| 146 :param initial_storage_size: The initial storage to allocate for the instance. | |
| 147 This only applies if ``cluster_type`` is set | |
| 148 to either ``Galaxy`` or ``Data`` and ``galaxy_data_option`` | |
| 149 is set to ``custom_size`` | |
| 150 | |
| 151 :type block_until_ready: bool | |
| 152 :param block_until_ready: Specifies whether the launch method will block | |
| 153 until the instance is ready and only return once | |
| 154 all initialization is complete. The default is False. | |
| 155 If False, the launch method will return immediately | |
| 156 without blocking. However, any subsequent calls | |
| 157 made will automatically block if the instance is | |
| 158 not ready and initialized. The blocking timeout | |
| 159 and polling interval can be configured by providing | |
| 160 extra parameters to the ``CloudManInstance.launch_instance`` | |
| 161 method. | |
| 162 """ | |
| 163 if security_groups is None: | |
| 164 security_groups = ['CloudMan'] | |
| 165 self.set_connection_parameters(access_key, secret_key, cloud_metadata) | |
| 166 self.set_pre_launch_parameters( | |
| 167 cluster_name, image_id, instance_type, | |
| 168 password, kernel_id, ramdisk_id, key_name, security_groups, | |
| 169 placement, block_until_ready) | |
| 170 self.set_post_launch_parameters(cluster_type, galaxy_data_option, initial_storage_size) | |
| 171 self.set_extra_parameters(**kwargs) | |
| 172 | |
| 173 def set_connection_parameters(self, access_key, secret_key, cloud_metadata=None): | |
| 174 self.access_key = access_key | |
| 175 self.secret_key = secret_key | |
| 176 self.cloud_metadata = cloud_metadata | |
| 177 | |
| 178 def set_pre_launch_parameters( | |
| 179 self, cluster_name, image_id, instance_type, password, | |
| 180 kernel_id=None, ramdisk_id=None, key_name='cloudman_key_pair', | |
| 181 security_groups=None, placement='', block_until_ready=False): | |
| 182 if security_groups is None: | |
| 183 security_groups = ['CloudMan'] | |
| 184 self.cluster_name = cluster_name | |
| 185 self.image_id = image_id | |
| 186 self.instance_type = instance_type | |
| 187 self.password = password | |
| 188 self.kernel_id = kernel_id | |
| 189 self.ramdisk_id = ramdisk_id | |
| 190 self.key_name = key_name | |
| 191 self.security_groups = security_groups | |
| 192 self.placement = placement | |
| 193 self.block_until_ready = block_until_ready | |
| 194 | |
| 195 def set_post_launch_parameters(self, cluster_type=None, galaxy_data_option='', initial_storage_size=10): | |
| 196 self.cluster_type = cluster_type | |
| 197 self.galaxy_data_option = galaxy_data_option | |
| 198 self.initial_storage_size = initial_storage_size | |
| 199 | |
| 200 def set_extra_parameters(self, **kwargs): | |
| 201 self.kwargs = kwargs | |
| 202 | |
| 203 class CustomTypeEncoder(json.JSONEncoder): | |
| 204 def default(self, obj): | |
| 205 if isinstance(obj, (CloudManConfig, Bunch)): | |
| 206 key = '__%s__' % obj.__class__.__name__ | |
| 207 return {key: obj.__dict__} | |
| 208 return json.JSONEncoder.default(self, obj) | |
| 209 | |
| 210 @staticmethod | |
| 211 def CustomTypeDecoder(dct): | |
| 212 if '__CloudManConfig__' in dct: | |
| 213 return CloudManConfig(**dct['__CloudManConfig__']) | |
| 214 elif '__Bunch__' in dct: | |
| 215 return Bunch(**dct['__Bunch__']) | |
| 216 else: | |
| 217 return dct | |
| 218 | |
| 219 @staticmethod | |
| 220 def load_config(fp): | |
| 221 return json.load(fp, object_hook=CloudManConfig.CustomTypeDecoder) | |
| 222 | |
| 223 def save_config(self, fp): | |
| 224 json.dump(self, fp, cls=self.CustomTypeEncoder) | |
| 225 | |
| 226 def validate(self): | |
| 227 if self.access_key is None: | |
| 228 return "Access key must not be null" | |
| 229 elif self.secret_key is None: | |
| 230 return "Secret key must not be null" | |
| 231 elif self.cluster_name is None: | |
| 232 return "Cluster name must not be null" | |
| 233 elif self.image_id is None: | |
| 234 return "Image ID must not be null" | |
| 235 elif self.instance_type is None: | |
| 236 return "Instance type must not be null" | |
| 237 elif self.password is None: | |
| 238 return "Password must not be null" | |
| 239 elif self.cluster_type not in [None, 'Test', 'Data', 'Galaxy', 'Shared_cluster']: | |
| 240 return "Unrecognized cluster type ({0})".format(self.cluster_type) | |
| 241 elif self.galaxy_data_option not in [None, '', 'custom-size', 'transient']: | |
| 242 return "Unrecognized galaxy data option ({0})".format(self.galaxy_data_option) | |
| 243 elif self.key_name is None: | |
| 244 return "Key-pair name must not be null" | |
| 245 else: | |
| 246 return None | |
| 247 | |
| 248 | |
| 249 class GenericVMInstance(object): | |
| 250 | |
| 251 def __init__(self, launcher, launch_result): | |
| 252 """ | |
| 253 Create an instance of the CloudMan API class, which is to be used when | |
| 254 manipulating that given CloudMan instance. | |
| 255 | |
| 256 The ``url`` is a string defining the address of CloudMan, for | |
| 257 example "http://115.146.92.174". The ``password`` is CloudMan's password, | |
| 258 as defined in the user data sent to CloudMan on instance creation. | |
| 259 """ | |
| 260 # Make sure the url scheme is defined (otherwise requests will not work) | |
| 261 self.vm_error = None | |
| 262 self.vm_status = None | |
| 263 self.host_name = None | |
| 264 self.launcher = launcher | |
| 265 self.launch_result = launch_result | |
| 266 | |
| 267 def _update_host_name(self, host_name): | |
| 268 if self.host_name != host_name: | |
| 269 self.host_name = host_name | |
| 270 | |
| 271 @property | |
| 272 def instance_id(self): | |
| 273 """ | |
| 274 Returns the ID of this instance (e.g., ``i-87ey32dd``) if launch was | |
| 275 successful or ``None`` otherwise. | |
| 276 """ | |
| 277 return None if self.launch_result is None else self.launch_result['instance_id'] | |
| 278 | |
| 279 @property | |
| 280 def key_pair_name(self): | |
| 281 """ | |
| 282 Returns the name of the key pair used by this instance. If instance was | |
| 283 not launched properly, returns ``None``. | |
| 284 """ | |
| 285 return None if self.launch_result is None else self.launch_result['kp_name'] | |
| 286 | |
| 287 @property | |
| 288 def key_pair_material(self): | |
| 289 """ | |
| 290 Returns the private portion of the generated key pair. It does so only | |
| 291 if the instance was properly launched and key pair generated; ``None`` | |
| 292 otherwise. | |
| 293 """ | |
| 294 return None if self.launch_result is None else self.launch_result['kp_material'] | |
| 295 | |
| 296 def get_machine_status(self): | |
| 297 """ | |
| 298 Check on the underlying VM status of an instance. This can be used to | |
| 299 determine whether the VM has finished booting up and if CloudMan | |
| 300 is up and running. | |
| 301 | |
| 302 Return a ``state`` dict with the current ``instance_state``, ``public_ip``, | |
| 303 ``placement``, and ``error`` keys, which capture the current state (the | |
| 304 values for those keys default to empty string if no data is available from | |
| 305 the cloud). | |
| 306 """ | |
| 307 if self.launcher: | |
| 308 return self.launcher.get_status(self.instance_id) | |
| 309 # elif self.host_name: | |
| 310 | |
| 311 else: | |
| 312 state = {'instance_state': "", | |
| 313 'public_ip': "", | |
| 314 'placement': "", | |
| 315 'error': "No reference to the instance object"} | |
| 316 return state | |
| 317 | |
| 318 def _init_instance(self, host_name): | |
| 319 self._update_host_name(host_name) | |
| 320 | |
| 321 def wait_until_instance_ready(self, vm_ready_timeout=300, vm_ready_check_interval=10): | |
| 322 """ | |
| 323 Wait until the VM state changes to ready/error or timeout elapses. | |
| 324 Updates the host name once ready. | |
| 325 """ | |
| 326 assert vm_ready_timeout > 0 | |
| 327 assert vm_ready_timeout > vm_ready_check_interval | |
| 328 assert vm_ready_check_interval > 0 | |
| 329 | |
| 330 if self.host_name: # Host name available. Therefore, instance is ready | |
| 331 return | |
| 332 | |
| 333 for time_left in range(vm_ready_timeout, 0, -vm_ready_check_interval): | |
| 334 status = self.get_machine_status() | |
| 335 if status['public_ip'] != '' and status['error'] == '': | |
| 336 self._init_instance(status['public_ip']) | |
| 337 return | |
| 338 elif status['error'] != '': | |
| 339 msg = "Error launching an instance: {0}".format(status['error']) | |
| 340 bioblend.log.error(msg) | |
| 341 raise VMLaunchException(msg) | |
| 342 else: | |
| 343 bioblend.log.warning("Instance not ready yet (it's in state '{0}'); waiting another {1} seconds..." | |
| 344 .format(status['instance_state'], time_left)) | |
| 345 time.sleep(vm_ready_check_interval) | |
| 346 | |
| 347 raise VMLaunchException("Waited too long for instance to become ready. Instance Id: %s" | |
| 348 % self.instance_id) | |
| 349 | |
| 350 | |
| 351 class CloudManInstance(GenericVMInstance): | |
| 352 | |
| 353 def __init__(self, url, password, **kwargs): | |
| 354 """ | |
| 355 Create an instance of the CloudMan API class, which is to be used when | |
| 356 manipulating that given CloudMan instance. | |
| 357 | |
| 358 The ``url`` is a string defining the address of CloudMan, for | |
| 359 example "http://115.146.92.174". The ``password`` is CloudMan's password, | |
| 360 as defined in the user data sent to CloudMan on instance creation. | |
| 361 """ | |
| 362 self.initialized = False | |
| 363 if kwargs.get('launch_result', None) is not None: # Used internally by the launch_instance method | |
| 364 super().__init__(kwargs['launcher'], kwargs['launch_result']) | |
| 365 else: | |
| 366 super().__init__(None, None) | |
| 367 self.config = kwargs.pop('cloudman_config', None) | |
| 368 if not self.config: | |
| 369 self.password = password | |
| 370 else: | |
| 371 self.password = self.config.password | |
| 372 self._set_url(url) | |
| 373 | |
| 374 def __repr__(self): | |
| 375 if self.cloudman_url: | |
| 376 return "CloudMan instance at {0}".format(self.cloudman_url) | |
| 377 else: | |
| 378 return "Waiting for this CloudMan instance to start..." | |
| 379 | |
| 380 def _update_host_name(self, host_name): | |
| 381 """ | |
| 382 Overrides the super-class method and makes sure that the ``cloudman_url`` | |
| 383 is kept in sync with the host name. | |
| 384 """ | |
| 385 self._set_url(host_name) | |
| 386 | |
| 387 def _init_instance(self, hostname): | |
| 388 super()._init_instance(hostname) | |
| 389 if self.config.cluster_type: | |
| 390 self.initialize(self.config.cluster_type, galaxy_data_option=self.config.galaxy_data_option, initial_storage_size=self.config.initial_storage_size) | |
| 391 | |
| 392 def _set_url(self, url): | |
| 393 """ | |
| 394 Keeps the CloudMan URL as well and the hostname in sync. | |
| 395 """ | |
| 396 if url: | |
| 397 parse_result = urlparse(url) | |
| 398 # Make sure the URL scheme is defined (otherwise requests will not work) | |
| 399 if not parse_result.scheme: | |
| 400 url = "http://" + url | |
| 401 # Parse the corrected URL again to extract the hostname | |
| 402 parse_result = urlparse(url) | |
| 403 super()._update_host_name(parse_result.hostname) | |
| 404 self.url = url | |
| 405 | |
| 406 @property | |
| 407 def galaxy_url(self): | |
| 408 """ | |
| 409 Returns the base URL for this instance, which by default happens to be | |
| 410 the URL for Galaxy application. | |
| 411 """ | |
| 412 return self.url | |
| 413 | |
| 414 @property | |
| 415 def cloudman_url(self): | |
| 416 """ | |
| 417 Returns the URL for accessing this instance of CloudMan. | |
| 418 """ | |
| 419 if self.url: | |
| 420 return self.url + '/cloud' | |
| 421 return None | |
| 422 | |
| 423 @staticmethod | |
| 424 def launch_instance(cfg, **kwargs): | |
| 425 """ | |
| 426 Launches a new instance of CloudMan on the specified cloud infrastructure. | |
| 427 | |
| 428 :type cfg: CloudManConfig | |
| 429 :param cfg: A CloudManConfig object containing the initial parameters | |
| 430 for this launch. | |
| 431 """ | |
| 432 validation_result = cfg.validate() | |
| 433 if validation_result is not None: | |
| 434 raise VMLaunchException( | |
| 435 "Invalid CloudMan configuration provided: {0}" | |
| 436 .format(validation_result)) | |
| 437 launcher = CloudManLauncher(cfg.access_key, cfg.secret_key, cfg.cloud_metadata) | |
| 438 result = launcher.launch( | |
| 439 cfg.cluster_name, cfg.image_id, cfg.instance_type, cfg.password, | |
| 440 cfg.kernel_id, cfg.ramdisk_id, cfg.key_name, cfg.security_groups, | |
| 441 cfg.placement, **cfg.kwargs) | |
| 442 if result['error'] is not None: | |
| 443 raise VMLaunchException("Error launching cloudman instance: {0}".format(result['error'])) | |
| 444 instance = CloudManInstance(None, None, launcher=launcher, | |
| 445 launch_result=result, cloudman_config=cfg) | |
| 446 if cfg.block_until_ready and cfg.cluster_type: | |
| 447 instance.get_status() # this will indirect result in initialize being invoked | |
| 448 return instance | |
| 449 | |
| 450 def update(self): | |
| 451 """ | |
| 452 Update the local object's fields to be in sync with the actual state | |
| 453 of the CloudMan instance the object points to. This method should be | |
| 454 called periodically to ensure you are looking at the current data. | |
| 455 | |
| 456 .. versionadded:: 0.2.2 | |
| 457 """ | |
| 458 ms = self.get_machine_status() | |
| 459 # Check if the machine is running and update IP and state | |
| 460 self.vm_status = ms.get('instance_state', None) | |
| 461 self.vm_error = ms.get('error', None) | |
| 462 public_ip = ms.get('public_ip', None) | |
| 463 # Update url if we don't have it or is different than what we have | |
| 464 if not self.url and (public_ip and self.url != public_ip): | |
| 465 self._set_url(public_ip) | |
| 466 # See if the cluster has been initialized | |
| 467 if self.vm_status == 'running' or self.url: | |
| 468 ct = self.get_cluster_type() | |
| 469 if ct.get('cluster_type', None): | |
| 470 self.initialized = True | |
| 471 if self.vm_error: | |
| 472 bioblend.log.error(self.vm_error) | |
| 473 | |
| 474 @block_until_vm_ready | |
| 475 def get_cloudman_version(self): | |
| 476 """ | |
| 477 Returns the cloudman version from the server. Versions prior to Cloudman 2 does not | |
| 478 support this call, and therefore, the default is to return 1 | |
| 479 """ | |
| 480 try: | |
| 481 r = self._make_get_request("cloudman_version") | |
| 482 return r['version'] | |
| 483 except Exception: | |
| 484 return 1 | |
| 485 | |
| 486 @block_until_vm_ready | |
| 487 def initialize(self, cluster_type, galaxy_data_option='', initial_storage_size=None, shared_bucket=None): | |
| 488 """ | |
| 489 Initialize CloudMan platform. This needs to be done before the cluster | |
| 490 can be used. | |
| 491 | |
| 492 The ``cluster_type``, either 'Galaxy', 'Data', or 'Test', defines the type | |
| 493 of cluster platform to initialize. | |
| 494 """ | |
| 495 if not self.initialized: | |
| 496 if self.get_cloudman_version() < 2: | |
| 497 r = self._make_get_request( | |
| 498 "initialize_cluster", | |
| 499 parameters={ | |
| 500 'startup_opt': cluster_type, | |
| 501 'g_pss': initial_storage_size, | |
| 502 'shared_bucket': shared_bucket | |
| 503 }) | |
| 504 else: | |
| 505 r = self._make_get_request( | |
| 506 "initialize_cluster", | |
| 507 parameters={ | |
| 508 'startup_opt': cluster_type, | |
| 509 'galaxy_data_option': galaxy_data_option, | |
| 510 'pss': initial_storage_size, | |
| 511 'shared_bucket': shared_bucket | |
| 512 }) | |
| 513 self.initialized = True | |
| 514 return r | |
| 515 | |
| 516 @block_until_vm_ready | |
| 517 def get_cluster_type(self): | |
| 518 """ | |
| 519 Get the ``cluster type`` for this CloudMan instance. See the | |
| 520 CloudMan docs about the available types. Returns a dictionary, | |
| 521 for example: ``{'cluster_type': 'Test'}``. | |
| 522 """ | |
| 523 cluster_type = self._make_get_request("cluster_type") | |
| 524 if cluster_type['cluster_type']: | |
| 525 self.initialized = True | |
| 526 return cluster_type | |
| 527 | |
| 528 @block_until_vm_ready | |
| 529 def get_status(self): | |
| 530 """ | |
| 531 Get status information on this CloudMan instance. | |
| 532 """ | |
| 533 return self._make_get_request("instance_state_json") | |
| 534 | |
| 535 @block_until_vm_ready | |
| 536 def get_nodes(self): | |
| 537 """ | |
| 538 Get a list of nodes currently running in this CloudMan cluster. | |
| 539 """ | |
| 540 instance_feed_json = self._make_get_request("instance_feed_json") | |
| 541 return instance_feed_json['instances'] | |
| 542 | |
| 543 @block_until_vm_ready | |
| 544 def get_cluster_size(self): | |
| 545 """ | |
| 546 Get the size of the cluster in terms of the number of nodes; this count | |
| 547 includes the master node. | |
| 548 """ | |
| 549 return len(self.get_nodes()) | |
| 550 | |
| 551 @block_until_vm_ready | |
| 552 def get_static_state(self): | |
| 553 """ | |
| 554 Get static information on this CloudMan instance. | |
| 555 i.e. state that doesn't change over the lifetime of the cluster | |
| 556 """ | |
| 557 return self._make_get_request("static_instance_state_json") | |
| 558 | |
| 559 @block_until_vm_ready | |
| 560 def get_master_ip(self): | |
| 561 """ | |
| 562 Returns the public IP of the master node in this CloudMan cluster | |
| 563 """ | |
| 564 status_json = self.get_static_state() | |
| 565 return status_json['master_ip'] | |
| 566 | |
| 567 @block_until_vm_ready | |
| 568 def get_master_id(self): | |
| 569 """ | |
| 570 Returns the instance ID of the master node in this CloudMan cluster | |
| 571 """ | |
| 572 status_json = self.get_static_state() | |
| 573 return status_json['master_id'] | |
| 574 | |
| 575 @block_until_vm_ready | |
| 576 def add_nodes(self, num_nodes, instance_type='', spot_price=''): | |
| 577 """ | |
| 578 Add a number of worker nodes to the cluster, optionally specifying | |
| 579 the type for new instances. If ``instance_type`` is not specified, | |
| 580 instance(s) of the same type as the master instance will be started. | |
| 581 Note that the ``instance_type`` must match the type of instance | |
| 582 available on the given cloud. | |
| 583 | |
| 584 ``spot_price`` applies only to AWS and, if set, defines the maximum | |
| 585 price for Spot instances, thus turning this request for more instances | |
| 586 into a Spot request. | |
| 587 """ | |
| 588 payload = {'number_nodes': num_nodes, | |
| 589 'instance_type': instance_type, | |
| 590 'spot_price': spot_price} | |
| 591 return self._make_get_request("add_instances", parameters=payload) | |
| 592 | |
| 593 @block_until_vm_ready | |
| 594 def remove_nodes(self, num_nodes, force=False): | |
| 595 """ | |
| 596 Remove worker nodes from the cluster. | |
| 597 | |
| 598 The ``num_nodes`` parameter defines the number of worker nodes to remove. | |
| 599 The ``force`` parameter (defaulting to False), is a boolean indicating | |
| 600 whether the nodes should be forcibly removed rather than gracefully removed. | |
| 601 """ | |
| 602 payload = {'number_nodes': num_nodes, 'force_termination': force} | |
| 603 result = self._make_get_request("remove_instances", parameters=payload) | |
| 604 return result | |
| 605 | |
| 606 @block_until_vm_ready | |
| 607 def remove_node(self, instance_id, force=False): | |
| 608 """ | |
| 609 Remove a specific worker node from the cluster. | |
| 610 | |
| 611 The ``instance_id`` parameter defines the ID, as a string, of a worker node | |
| 612 to remove from the cluster. The ``force`` parameter (defaulting to False), | |
| 613 is a boolean indicating whether the node should be forcibly removed rather | |
| 614 than gracefully removed. | |
| 615 | |
| 616 """ | |
| 617 payload = {'instance_id': instance_id} | |
| 618 return self._make_get_request("remove_instance", parameters=payload) | |
| 619 | |
| 620 @block_until_vm_ready | |
| 621 def reboot_node(self, instance_id): | |
| 622 """ | |
| 623 Reboot a specific worker node. | |
| 624 | |
| 625 The ``instance_id`` parameter defines the ID, as a string, of a worker node | |
| 626 to reboot. | |
| 627 """ | |
| 628 payload = {'instance_id': instance_id} | |
| 629 return self._make_get_request("reboot_instance", parameters=payload) | |
| 630 | |
| 631 @block_until_vm_ready | |
| 632 def autoscaling_enabled(self): | |
| 633 """ | |
| 634 Returns a boolean indicating whether autoscaling is enabled. | |
| 635 """ | |
| 636 return bool(self.get_status()['autoscaling']['use_autoscaling']) | |
| 637 | |
| 638 @block_until_vm_ready | |
| 639 def enable_autoscaling(self, minimum_nodes=0, maximum_nodes=19): | |
| 640 """ | |
| 641 Enable cluster autoscaling, allowing the cluster to automatically add, | |
| 642 or remove, worker nodes, as needed. | |
| 643 | |
| 644 The number of worker nodes in the cluster is bounded by the ``minimum_nodes`` | |
| 645 (default is 0) and ``maximum_nodes`` (default is 19) parameters. | |
| 646 """ | |
| 647 if not self.autoscaling_enabled(): | |
| 648 payload = {'as_min': minimum_nodes, 'as_max': maximum_nodes} | |
| 649 self._make_get_request("toggle_autoscaling", parameters=payload) | |
| 650 | |
| 651 @block_until_vm_ready | |
| 652 def disable_autoscaling(self): | |
| 653 """ | |
| 654 Disable autoscaling, meaning that worker nodes will need to be manually | |
| 655 added and removed. | |
| 656 """ | |
| 657 if self.autoscaling_enabled(): | |
| 658 self._make_get_request("toggle_autoscaling") | |
| 659 | |
| 660 @block_until_vm_ready | |
| 661 def adjust_autoscaling(self, minimum_nodes=None, maximum_nodes=None): | |
| 662 """ | |
| 663 Adjust the autoscaling configuration parameters. | |
| 664 | |
| 665 The number of worker nodes in the cluster is bounded by the optional | |
| 666 ``minimum_nodes`` and ``maximum_nodes`` parameters. If a parameter is | |
| 667 not provided then its configuration value does not change. | |
| 668 """ | |
| 669 if self.autoscaling_enabled(): | |
| 670 payload = {'as_min_adj': minimum_nodes, 'as_max_adj': maximum_nodes} | |
| 671 self._make_get_request("adjust_autoscaling", parameters=payload) | |
| 672 | |
| 673 @block_until_vm_ready | |
| 674 def is_master_execution_host(self): | |
| 675 """ | |
| 676 Checks whether the master node has job execution enabled. | |
| 677 | |
| 678 """ | |
| 679 status = self._make_get_request("get_all_services_status") | |
| 680 return bool(status['master_is_exec_host']) | |
| 681 | |
| 682 @block_until_vm_ready | |
| 683 def set_master_as_execution_host(self, enable): | |
| 684 """ | |
| 685 Enables/disables master as execution host. | |
| 686 | |
| 687 """ | |
| 688 if not self.is_master_execution_host(): | |
| 689 self._make_get_request("toggle_master_as_exec_host") | |
| 690 | |
| 691 @block_until_vm_ready | |
| 692 def get_galaxy_state(self): | |
| 693 """ | |
| 694 Get the current status of Galaxy running on the cluster. | |
| 695 """ | |
| 696 payload = {'srvc': 'Galaxy'} | |
| 697 status = self._make_get_request("get_srvc_status", parameters=payload) | |
| 698 return {'status': status['status']} | |
| 699 | |
| 700 @block_until_vm_ready | |
| 701 def terminate(self, terminate_master_instance=True, delete_cluster=False): | |
| 702 """ | |
| 703 Terminate this CloudMan cluster. There is an option to also terminate the | |
| 704 master instance (all worker instances will be terminated in the process | |
| 705 of cluster termination), and delete the whole cluster. | |
| 706 | |
| 707 .. warning:: | |
| 708 Deleting a cluster is irreversible - all of the data will be | |
| 709 permanently deleted. | |
| 710 """ | |
| 711 payload = {'terminate_master_instance': terminate_master_instance, | |
| 712 'delete_cluster': delete_cluster} | |
| 713 result = self._make_get_request("kill_all", parameters=payload, | |
| 714 timeout=15) | |
| 715 return result | |
| 716 | |
| 717 def _make_get_request(self, url, parameters=None, timeout=None): | |
| 718 """ | |
| 719 Private function that makes a GET request to the nominated ``url``, | |
| 720 with the provided GET ``parameters``. Optionally, set the ``timeout`` | |
| 721 to stop waiting for a response after a given number of seconds. This is | |
| 722 particularly useful when terminating a cluster as it may terminate | |
| 723 before sending a response. | |
| 724 """ | |
| 725 if parameters is None: | |
| 726 parameters = {} | |
| 727 req_url = '/'.join((self.cloudman_url, 'root', url)) | |
| 728 r = requests.get(req_url, params=parameters, auth=("", self.password), timeout=timeout) | |
| 729 try: | |
| 730 json = r.json() | |
| 731 return json | |
| 732 except Exception: | |
| 733 return r.text |
