1""" 2SoftLayer Cloud Module 3====================== 4 5The SoftLayer cloud module is used to control access to the SoftLayer VPS 6system. 7 8Use of this module only requires the ``apikey`` parameter. Set up the cloud 9configuration at: 10 11``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/softlayer.conf``: 12 13.. code-block:: yaml 14 15 my-softlayer-config: 16 # SoftLayer account api key 17 user: MYLOGIN 18 apikey: JVkbSJDGHSDKUKSDJfhsdklfjgsjdkflhjlsdfffhgdgjkenrtuinv 19 driver: softlayer 20 21The SoftLayer Python Library needs to be installed in order to use the 22SoftLayer salt.cloud modules. See: https://pypi.python.org/pypi/SoftLayer 23 24:depends: softlayer 25""" 26 27import logging 28import time 29 30import salt.config as config 31import salt.utils.cloud 32from salt.exceptions import SaltCloudSystemExit 33 34# Attempt to import softlayer lib 35try: 36 import SoftLayer 37 38 HAS_SLLIBS = True 39except ImportError: 40 HAS_SLLIBS = False 41 42# Get logging started 43log = logging.getLogger(__name__) 44 45__virtualname__ = "softlayer" 46 47 48# Only load in this module if the SoftLayer configurations are in place 49def __virtual__(): 50 """ 51 Check for SoftLayer configurations. 52 """ 53 if get_configured_provider() is False: 54 return False 55 56 if get_dependencies() is False: 57 return False 58 59 return __virtualname__ 60 61 62def _get_active_provider_name(): 63 try: 64 return __active_provider_name__.value() 65 except AttributeError: 66 return __active_provider_name__ 67 68 69def get_configured_provider(): 70 """ 71 Return the first configured instance. 72 """ 73 return config.is_provider_configured( 74 __opts__, _get_active_provider_name() or __virtualname__, ("apikey",) 75 ) 76 77 78def get_dependencies(): 79 """ 80 Warn if dependencies aren't met. 81 """ 82 return config.check_driver_dependencies(__virtualname__, {"softlayer": HAS_SLLIBS}) 83 84 85def script(vm_): 86 """ 87 Return the script deployment object 88 """ 89 deploy_script = salt.utils.cloud.os_script( 90 config.get_cloud_config_value("script", vm_, __opts__), 91 vm_, 92 __opts__, 93 salt.utils.cloud.salt_config_to_yaml( 94 salt.utils.cloud.minion_config(__opts__, vm_) 95 ), 96 ) 97 return deploy_script 98 99 100def get_conn(service="SoftLayer_Virtual_Guest"): 101 """ 102 Return a conn object for the passed VM data 103 """ 104 client = SoftLayer.Client( 105 username=config.get_cloud_config_value( 106 "user", get_configured_provider(), __opts__, search_global=False 107 ), 108 api_key=config.get_cloud_config_value( 109 "apikey", get_configured_provider(), __opts__, search_global=False 110 ), 111 ) 112 return client[service] 113 114 115def avail_locations(call=None): 116 """ 117 List all available locations 118 """ 119 if call == "action": 120 raise SaltCloudSystemExit( 121 "The avail_locations function must be called with " 122 "-f or --function, or with the --list-locations option" 123 ) 124 125 ret = {} 126 conn = get_conn() 127 response = conn.getCreateObjectOptions() 128 # return response 129 for datacenter in response["datacenters"]: 130 # return data center 131 ret[datacenter["template"]["datacenter"]["name"]] = { 132 "name": datacenter["template"]["datacenter"]["name"], 133 } 134 return ret 135 136 137def avail_sizes(call=None): 138 """ 139 Return a dict of all available VM sizes on the cloud provider with 140 relevant data. This data is provided in three dicts. 141 """ 142 if call == "action": 143 raise SaltCloudSystemExit( 144 "The avail_sizes function must be called with " 145 "-f or --function, or with the --list-sizes option" 146 ) 147 148 ret = { 149 "block devices": {}, 150 "memory": {}, 151 "processors": {}, 152 } 153 conn = get_conn() 154 response = conn.getCreateObjectOptions() 155 for device in response["blockDevices"]: 156 # return device['template']['blockDevices'] 157 ret["block devices"][device["itemPrice"]["item"]["description"]] = { 158 "name": device["itemPrice"]["item"]["description"], 159 "capacity": device["template"]["blockDevices"][0]["diskImage"]["capacity"], 160 } 161 for memory in response["memory"]: 162 ret["memory"][memory["itemPrice"]["item"]["description"]] = { 163 "name": memory["itemPrice"]["item"]["description"], 164 "maxMemory": memory["template"]["maxMemory"], 165 } 166 for processors in response["processors"]: 167 ret["processors"][processors["itemPrice"]["item"]["description"]] = { 168 "name": processors["itemPrice"]["item"]["description"], 169 "start cpus": processors["template"]["startCpus"], 170 } 171 return ret 172 173 174def avail_images(call=None): 175 """ 176 Return a dict of all available VM images on the cloud provider. 177 """ 178 if call == "action": 179 raise SaltCloudSystemExit( 180 "The avail_images function must be called with " 181 "-f or --function, or with the --list-images option" 182 ) 183 184 ret = {} 185 conn = get_conn() 186 response = conn.getCreateObjectOptions() 187 for image in response["operatingSystems"]: 188 ret[image["itemPrice"]["item"]["description"]] = { 189 "name": image["itemPrice"]["item"]["description"], 190 "template": image["template"]["operatingSystemReferenceCode"], 191 } 192 return ret 193 194 195def list_custom_images(call=None): 196 """ 197 Return a dict of all custom VM images on the cloud provider. 198 """ 199 if call != "function": 200 raise SaltCloudSystemExit( 201 "The list_vlans function must be called with -f or --function." 202 ) 203 204 ret = {} 205 conn = get_conn("SoftLayer_Account") 206 response = conn.getBlockDeviceTemplateGroups() 207 for image in response: 208 if "globalIdentifier" not in image: 209 continue 210 ret[image["name"]] = { 211 "id": image["id"], 212 "name": image["name"], 213 "globalIdentifier": image["globalIdentifier"], 214 } 215 if "note" in image: 216 ret[image["name"]]["note"] = image["note"] 217 return ret 218 219 220def get_location(vm_=None): 221 """ 222 Return the location to use, in this order: 223 - CLI parameter 224 - VM parameter 225 - Cloud profile setting 226 """ 227 return __opts__.get( 228 "location", 229 config.get_cloud_config_value( 230 "location", 231 vm_ or get_configured_provider(), 232 __opts__, 233 # default=DEFAULT_LOCATION, 234 search_global=False, 235 ), 236 ) 237 238 239def create(vm_): 240 """ 241 Create a single VM from a data dict 242 """ 243 try: 244 # Check for required profile parameters before sending any API calls. 245 if ( 246 vm_["profile"] 247 and config.is_profile_configured( 248 __opts__, 249 _get_active_provider_name() or "softlayer", 250 vm_["profile"], 251 vm_=vm_, 252 ) 253 is False 254 ): 255 return False 256 except AttributeError: 257 pass 258 259 name = vm_["name"] 260 hostname = name 261 domain = config.get_cloud_config_value("domain", vm_, __opts__, default=None) 262 if domain is None: 263 SaltCloudSystemExit("A domain name is required for the SoftLayer driver.") 264 265 if vm_.get("use_fqdn"): 266 name = ".".join([name, domain]) 267 vm_["name"] = name 268 269 __utils__["cloud.fire_event"]( 270 "event", 271 "starting create", 272 "salt/cloud/{}/creating".format(name), 273 args=__utils__["cloud.filter_event"]( 274 "creating", vm_, ["name", "profile", "provider", "driver"] 275 ), 276 sock_dir=__opts__["sock_dir"], 277 transport=__opts__["transport"], 278 ) 279 280 log.info("Creating Cloud VM %s", name) 281 conn = get_conn() 282 kwargs = { 283 "hostname": hostname, 284 "domain": domain, 285 "startCpus": vm_["cpu_number"], 286 "maxMemory": vm_["ram"], 287 "hourlyBillingFlag": vm_["hourly_billing"], 288 } 289 290 local_disk_flag = config.get_cloud_config_value( 291 "local_disk", vm_, __opts__, default=False 292 ) 293 kwargs["localDiskFlag"] = local_disk_flag 294 295 if "image" in vm_: 296 kwargs["operatingSystemReferenceCode"] = vm_["image"] 297 kwargs["blockDevices"] = [] 298 disks = vm_["disk_size"] 299 300 if isinstance(disks, int): 301 disks = [str(disks)] 302 elif isinstance(disks, str): 303 disks = [size.strip() for size in disks.split(",")] 304 305 count = 0 306 for disk in disks: 307 # device number '1' is reserved for the SWAP disk 308 if count == 1: 309 count += 1 310 block_device = { 311 "device": str(count), 312 "diskImage": {"capacity": str(disk)}, 313 } 314 kwargs["blockDevices"].append(block_device) 315 count += 1 316 317 # Upper bound must be 5 as we're skipping '1' for the SWAP disk ID 318 if count > 5: 319 log.warning( 320 "More that 5 disks were specified for %s ." 321 "The first 5 disks will be applied to the VM, " 322 "but the remaining disks will be ignored.\n" 323 "Please adjust your cloud configuration to only " 324 "specify a maximum of 5 disks.", 325 name, 326 ) 327 break 328 329 elif "global_identifier" in vm_: 330 kwargs["blockDeviceTemplateGroup"] = { 331 "globalIdentifier": vm_["global_identifier"] 332 } 333 334 location = get_location(vm_) 335 if location: 336 kwargs["datacenter"] = {"name": location} 337 338 private_vlan = config.get_cloud_config_value( 339 "private_vlan", vm_, __opts__, default=False 340 ) 341 if private_vlan: 342 kwargs["primaryBackendNetworkComponent"] = {"networkVlan": {"id": private_vlan}} 343 344 private_network = config.get_cloud_config_value( 345 "private_network", vm_, __opts__, default=False 346 ) 347 if bool(private_network) is True: 348 kwargs["privateNetworkOnlyFlag"] = "True" 349 350 public_vlan = config.get_cloud_config_value( 351 "public_vlan", vm_, __opts__, default=False 352 ) 353 if public_vlan: 354 kwargs["primaryNetworkComponent"] = {"networkVlan": {"id": public_vlan}} 355 356 public_security_groups = config.get_cloud_config_value( 357 "public_security_groups", vm_, __opts__, default=False 358 ) 359 if public_security_groups: 360 secgroups = [ 361 {"securityGroup": {"id": int(sg)}} for sg in public_security_groups 362 ] 363 pnc = kwargs.get("primaryNetworkComponent", {}) 364 pnc["securityGroupBindings"] = secgroups 365 kwargs.update({"primaryNetworkComponent": pnc}) 366 367 private_security_groups = config.get_cloud_config_value( 368 "private_security_groups", vm_, __opts__, default=False 369 ) 370 371 if private_security_groups: 372 secgroups = [ 373 {"securityGroup": {"id": int(sg)}} for sg in private_security_groups 374 ] 375 pbnc = kwargs.get("primaryBackendNetworkComponent", {}) 376 pbnc["securityGroupBindings"] = secgroups 377 kwargs.update({"primaryBackendNetworkComponent": pbnc}) 378 379 max_net_speed = config.get_cloud_config_value( 380 "max_net_speed", vm_, __opts__, default=10 381 ) 382 if max_net_speed: 383 kwargs["networkComponents"] = [{"maxSpeed": int(max_net_speed)}] 384 385 post_uri = config.get_cloud_config_value("post_uri", vm_, __opts__, default=None) 386 if post_uri: 387 kwargs["postInstallScriptUri"] = post_uri 388 389 dedicated_host_id = config.get_cloud_config_value( 390 "dedicated_host_id", vm_, __opts__, default=None 391 ) 392 if dedicated_host_id: 393 kwargs["dedicatedHost"] = {"id": dedicated_host_id} 394 395 __utils__["cloud.fire_event"]( 396 "event", 397 "requesting instance", 398 "salt/cloud/{}/requesting".format(name), 399 args={ 400 "kwargs": __utils__["cloud.filter_event"]( 401 "requesting", kwargs, list(kwargs) 402 ), 403 }, 404 sock_dir=__opts__["sock_dir"], 405 transport=__opts__["transport"], 406 ) 407 408 try: 409 response = conn.createObject(kwargs) 410 except Exception as exc: # pylint: disable=broad-except 411 log.error( 412 "Error creating %s on SoftLayer\n\n" 413 "The following exception was thrown when trying to " 414 "run the initial deployment: \n%s", 415 name, 416 exc, 417 # Show the traceback if the debug logging level is enabled 418 exc_info_on_loglevel=logging.DEBUG, 419 ) 420 return False 421 422 ip_type = "primaryIpAddress" 423 private_ssh = config.get_cloud_config_value( 424 "private_ssh", vm_, __opts__, default=False 425 ) 426 private_wds = config.get_cloud_config_value( 427 "private_windows", vm_, __opts__, default=False 428 ) 429 if private_ssh or private_wds or public_vlan is None: 430 ip_type = "primaryBackendIpAddress" 431 432 def wait_for_ip(): 433 """ 434 Wait for the IP address to become available 435 """ 436 nodes = list_nodes_full() 437 if ip_type in nodes[hostname]: 438 return nodes[hostname][ip_type] 439 time.sleep(1) 440 return False 441 442 ip_address = salt.utils.cloud.wait_for_fun( 443 wait_for_ip, 444 timeout=config.get_cloud_config_value( 445 "wait_for_fun_timeout", vm_, __opts__, default=15 * 60 446 ), 447 ) 448 if config.get_cloud_config_value("deploy", vm_, __opts__) is not True: 449 return show_instance(hostname, call="action") 450 451 SSH_PORT = 22 452 WINDOWS_DS_PORT = 445 453 managing_port = SSH_PORT 454 if config.get_cloud_config_value( 455 "windows", vm_, __opts__ 456 ) or config.get_cloud_config_value("win_installer", vm_, __opts__): 457 managing_port = WINDOWS_DS_PORT 458 459 ssh_connect_timeout = config.get_cloud_config_value( 460 "ssh_connect_timeout", vm_, __opts__, 15 * 60 461 ) 462 connect_timeout = config.get_cloud_config_value( 463 "connect_timeout", vm_, __opts__, ssh_connect_timeout 464 ) 465 if not salt.utils.cloud.wait_for_port( 466 ip_address, port=managing_port, timeout=connect_timeout 467 ): 468 raise SaltCloudSystemExit("Failed to authenticate against remote ssh") 469 470 pass_conn = get_conn(service="SoftLayer_Account") 471 mask = { 472 "virtualGuests": {"powerState": "", "operatingSystem": {"passwords": ""}}, 473 } 474 475 def get_credentials(): 476 """ 477 Wait for the password to become available 478 """ 479 node_info = pass_conn.getVirtualGuests(id=response["id"], mask=mask) 480 for node in node_info: 481 if ( 482 node["id"] == response["id"] 483 and "passwords" in node["operatingSystem"] 484 and node["operatingSystem"]["passwords"] 485 ): 486 return ( 487 node["operatingSystem"]["passwords"][0]["username"], 488 node["operatingSystem"]["passwords"][0]["password"], 489 ) 490 time.sleep(5) 491 return False 492 493 username, passwd = salt.utils.cloud.wait_for_fun( # pylint: disable=W0633 494 get_credentials, 495 timeout=config.get_cloud_config_value( 496 "wait_for_fun_timeout", vm_, __opts__, default=15 * 60 497 ), 498 ) 499 response["username"] = username 500 response["password"] = passwd 501 response["public_ip"] = ip_address 502 503 ssh_username = config.get_cloud_config_value( 504 "ssh_username", vm_, __opts__, default=username 505 ) 506 507 vm_["ssh_host"] = ip_address 508 vm_["password"] = passwd 509 ret = __utils__["cloud.bootstrap"](vm_, __opts__) 510 511 ret.update(response) 512 513 __utils__["cloud.fire_event"]( 514 "event", 515 "created instance", 516 "salt/cloud/{}/created".format(name), 517 args=__utils__["cloud.filter_event"]( 518 "created", vm_, ["name", "profile", "provider", "driver"] 519 ), 520 sock_dir=__opts__["sock_dir"], 521 transport=__opts__["transport"], 522 ) 523 524 return ret 525 526 527def list_nodes_full(mask="mask[id]", call=None): 528 """ 529 Return a list of the VMs that are on the provider 530 """ 531 if call == "action": 532 raise SaltCloudSystemExit( 533 "The list_nodes_full function must be called with -f or --function." 534 ) 535 536 ret = {} 537 conn = get_conn(service="SoftLayer_Account") 538 response = conn.getVirtualGuests() 539 for node_id in response: 540 hostname = node_id["hostname"] 541 ret[hostname] = node_id 542 __utils__["cloud.cache_node_list"]( 543 ret, _get_active_provider_name().split(":")[0], __opts__ 544 ) 545 return ret 546 547 548def list_nodes(call=None): 549 """ 550 Return a list of the VMs that are on the provider 551 """ 552 if call == "action": 553 raise SaltCloudSystemExit( 554 "The list_nodes function must be called with -f or --function." 555 ) 556 557 ret = {} 558 nodes = list_nodes_full() 559 if "error" in nodes: 560 raise SaltCloudSystemExit( 561 "An error occurred while listing nodes: {}".format( 562 nodes["error"]["Errors"]["Error"]["Message"] 563 ) 564 ) 565 for node in nodes: 566 ret[node] = { 567 "id": nodes[node]["hostname"], 568 "ram": nodes[node]["maxMemory"], 569 "cpus": nodes[node]["maxCpu"], 570 } 571 if "primaryIpAddress" in nodes[node]: 572 ret[node]["public_ips"] = nodes[node]["primaryIpAddress"] 573 if "primaryBackendIpAddress" in nodes[node]: 574 ret[node]["private_ips"] = nodes[node]["primaryBackendIpAddress"] 575 if "status" in nodes[node]: 576 ret[node]["state"] = str(nodes[node]["status"]["name"]) 577 return ret 578 579 580def list_nodes_select(call=None): 581 """ 582 Return a list of the VMs that are on the provider, with select fields 583 """ 584 return salt.utils.cloud.list_nodes_select( 585 list_nodes_full(), 586 __opts__["query.selection"], 587 call, 588 ) 589 590 591def show_instance(name, call=None): 592 """ 593 Show the details from SoftLayer concerning a guest 594 """ 595 if call != "action": 596 raise SaltCloudSystemExit( 597 "The show_instance action must be called with -a or --action." 598 ) 599 600 nodes = list_nodes_full() 601 __utils__["cloud.cache_node"](nodes[name], _get_active_provider_name(), __opts__) 602 return nodes[name] 603 604 605def destroy(name, call=None): 606 """ 607 Destroy a node. 608 609 CLI Example: 610 611 .. code-block:: bash 612 613 salt-cloud --destroy mymachine 614 """ 615 if call == "function": 616 raise SaltCloudSystemExit( 617 "The destroy action must be called with -d, --destroy, -a or --action." 618 ) 619 620 __utils__["cloud.fire_event"]( 621 "event", 622 "destroying instance", 623 "salt/cloud/{}/destroying".format(name), 624 args={"name": name}, 625 sock_dir=__opts__["sock_dir"], 626 transport=__opts__["transport"], 627 ) 628 629 node = show_instance(name, call="action") 630 conn = get_conn() 631 response = conn.deleteObject(id=node["id"]) 632 633 __utils__["cloud.fire_event"]( 634 "event", 635 "destroyed instance", 636 "salt/cloud/{}/destroyed".format(name), 637 args={"name": name}, 638 sock_dir=__opts__["sock_dir"], 639 transport=__opts__["transport"], 640 ) 641 if __opts__.get("update_cachedir", False) is True: 642 __utils__["cloud.delete_minion_cachedir"]( 643 name, _get_active_provider_name().split(":")[0], __opts__ 644 ) 645 646 return response 647 648 649def list_vlans(call=None): 650 """ 651 List all VLANs associated with the account 652 """ 653 if call != "function": 654 raise SaltCloudSystemExit( 655 "The list_vlans function must be called with -f or --function." 656 ) 657 658 conn = get_conn(service="SoftLayer_Account") 659 return conn.getNetworkVlans() 660