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