1"""
2CenturyLink Cloud Module
3========================
4
5.. versionadded:: 2018.3
6
7The CLC cloud module allows you to manage CLC Via the CLC SDK.
8
9:codeauthor: Stephan Looney <slooney@stephanlooney.com>
10
11
12Dependencies
13============
14
15- clc-sdk Python Module
16- flask
17
18CLC SDK
19-------
20
21clc-sdk can be installed via pip:
22
23.. code-block:: bash
24
25    pip install clc-sdk
26
27.. note::
28  For sdk reference see: https://github.com/CenturyLinkCloud/clc-python-sdk
29
30Flask
31-----
32
33flask can be installed via pip:
34
35.. code-block:: bash
36
37    pip install flask
38
39Configuration
40=============
41
42To use this module: set up the clc-sdk, user, password, key in the
43cloud configuration at
44``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/clc.conf``:
45
46.. code-block:: yaml
47
48    my-clc-config:
49      driver: clc
50      user: 'web-user'
51      password: 'verybadpass'
52      token: ''
53      token_pass:''
54      accountalias: 'ACT'
55.. note::
56
57    The ``provider`` parameter in cloud provider configuration was renamed to ``driver``.
58    This change was made to avoid confusion with the ``provider`` parameter that is
59    used in cloud profile configuration. Cloud provider configuration now uses ``driver``
60    to refer to the salt-cloud driver that provides the underlying functionality to
61    connect to a cloud provider, while cloud profile configuration continues to use
62    ``provider`` to refer to the cloud provider configuration that you define.
63
64"""
65
66import importlib
67import logging
68import time
69
70import salt.config as config
71import salt.utils.json
72from salt.exceptions import SaltCloudSystemExit
73
74# Attempt to import clc-sdk lib
75try:
76    # when running this in linode's Ubuntu 16.x version the following line is required
77    # to get the clc sdk libraries to load
78    importlib.import_module("clc")
79    import clc
80
81    HAS_CLC = True
82except ImportError:
83    HAS_CLC = False
84# Disable InsecureRequestWarning generated on python > 2.6
85try:
86    from requests.packages.urllib3 import (
87        disable_warnings,
88    )  # pylint: disable=no-name-in-module
89
90    disable_warnings()
91except Exception:  # pylint: disable=broad-except
92    pass
93
94log = logging.getLogger(__name__)
95
96
97__virtualname__ = "clc"
98
99
100# Only load in this module if the CLC configurations are in place
101def __virtual__():
102    """
103    Check for CLC configuration and if required libs are available.
104    """
105    if get_configured_provider() is False or get_dependencies() is False:
106        return False
107
108    return __virtualname__
109
110
111def _get_active_provider_name():
112    try:
113        return __active_provider_name__.value()
114    except AttributeError:
115        return __active_provider_name__
116
117
118def get_configured_provider():
119    return config.is_provider_configured(
120        __opts__,
121        _get_active_provider_name() or __virtualname__,
122        (
123            "token",
124            "token_pass",
125            "user",
126            "password",
127        ),
128    )
129
130
131def get_dependencies():
132    """
133    Warn if dependencies aren't met.
134    """
135    deps = {
136        "clc": HAS_CLC,
137    }
138    return config.check_driver_dependencies(__virtualname__, deps)
139
140
141def get_creds():
142    user = config.get_cloud_config_value(
143        "user", get_configured_provider(), __opts__, search_global=False
144    )
145    password = config.get_cloud_config_value(
146        "password", get_configured_provider(), __opts__, search_global=False
147    )
148    accountalias = config.get_cloud_config_value(
149        "accountalias", get_configured_provider(), __opts__, search_global=False
150    )
151    token = config.get_cloud_config_value(
152        "token", get_configured_provider(), __opts__, search_global=False
153    )
154    token_pass = config.get_cloud_config_value(
155        "token_pass", get_configured_provider(), __opts__, search_global=False
156    )
157    creds = {
158        "user": user,
159        "password": password,
160        "token": token,
161        "token_pass": token_pass,
162        "accountalias": accountalias,
163    }
164    return creds
165
166
167def list_nodes_full(call=None, for_output=True):
168    """
169    Return a list of the VMs that are on the provider
170    """
171    if call == "action":
172        raise SaltCloudSystemExit(
173            "The list_nodes_full function must be called with -f or --function."
174        )
175    creds = get_creds()
176    clc.v1.SetCredentials(creds["token"], creds["token_pass"])
177    servers_raw = clc.v1.Server.GetServers(location=None)
178    servers_raw = salt.utils.json.dumps(servers_raw)
179    servers = salt.utils.json.loads(servers_raw)
180    return servers
181
182
183def get_queue_data(call=None, for_output=True):
184    creds = get_creds()
185    clc.v1.SetCredentials(creds["token"], creds["token_pass"])
186    cl_queue = clc.v1.Queue.List()
187    return cl_queue
188
189
190def get_monthly_estimate(call=None, for_output=True):
191    """
192    Return a list of the VMs that are on the provider
193    """
194    creds = get_creds()
195    clc.v1.SetCredentials(creds["token"], creds["token_pass"])
196    if call == "action":
197        raise SaltCloudSystemExit(
198            "The list_nodes_full function must be called with -f or --function."
199        )
200    try:
201        billing_raw = clc.v1.Billing.GetAccountSummary(alias=creds["accountalias"])
202        billing_raw = salt.utils.json.dumps(billing_raw)
203        billing = salt.utils.json.loads(billing_raw)
204        billing = round(billing["MonthlyEstimate"], 2)
205        return {"Monthly Estimate": billing}
206    except RuntimeError:
207        return {"Monthly Estimate": 0}
208
209
210def get_month_to_date(call=None, for_output=True):
211    """
212    Return a list of the VMs that are on the provider
213    """
214    creds = get_creds()
215    clc.v1.SetCredentials(creds["token"], creds["token_pass"])
216    if call == "action":
217        raise SaltCloudSystemExit(
218            "The list_nodes_full function must be called with -f or --function."
219        )
220    try:
221        billing_raw = clc.v1.Billing.GetAccountSummary(alias=creds["accountalias"])
222        billing_raw = salt.utils.json.dumps(billing_raw)
223        billing = salt.utils.json.loads(billing_raw)
224        billing = round(billing["MonthToDateTotal"], 2)
225        return {"Month To Date": billing}
226    except RuntimeError:
227        return 0
228
229
230def get_server_alerts(call=None, for_output=True, **kwargs):
231    """
232    Return a list of alerts from CLC as reported by their infra
233    """
234    for key, value in kwargs.items():
235        servername = ""
236        if key == "servername":
237            servername = value
238    creds = get_creds()
239    clc.v2.SetCredentials(creds["user"], creds["password"])
240    alerts = clc.v2.Server(servername).Alerts()
241    return alerts
242
243
244def get_group_estimate(call=None, for_output=True, **kwargs):
245    """
246    Return a list of the VMs that are on the provider
247    usage: "salt-cloud -f get_group_estimate clc group=Dev location=VA1"
248    """
249    for key, value in kwargs.items():
250        group = ""
251        location = ""
252        if key == "group":
253            group = value
254        if key == "location":
255            location = value
256    creds = get_creds()
257    clc.v1.SetCredentials(creds["token"], creds["token_pass"])
258    if call == "action":
259        raise SaltCloudSystemExit(
260            "The list_nodes_full function must be called with -f or --function."
261        )
262    try:
263        billing_raw = clc.v1.Billing.GetGroupEstimate(
264            group=group, alias=creds["accountalias"], location=location
265        )
266        billing_raw = salt.utils.json.dumps(billing_raw)
267        billing = salt.utils.json.loads(billing_raw)
268        estimate = round(billing["MonthlyEstimate"], 2)
269        month_to_date = round(billing["MonthToDate"], 2)
270        return {"Monthly Estimate": estimate, "Month to Date": month_to_date}
271    except RuntimeError:
272        return 0
273
274
275def avail_images(call=None):
276    """
277    returns a list of images available to you
278    """
279    all_servers = list_nodes_full()
280    templates = {}
281    for server in all_servers:
282        if server["IsTemplate"]:
283            templates.update({"Template Name": server["Name"]})
284    return templates
285
286
287def avail_locations(call=None):
288    """
289    returns a list of locations available to you
290    """
291    creds = get_creds()
292    clc.v1.SetCredentials(creds["token"], creds["token_pass"])
293    locations = clc.v1.Account.GetLocations()
294    return locations
295
296
297def avail_sizes(call=None):
298    """
299    use templates for this
300    """
301    return {"Sizes": "Sizes are built into templates. Choose appropriate template"}
302
303
304def get_build_status(req_id, nodename):
305    """
306    get the build status from CLC to make sure we don't return to early
307    """
308    counter = 0
309    req_id = str(req_id)
310    while counter < 10:
311        queue = clc.v1.Blueprint.GetStatus(request_id=(req_id))
312        if queue["PercentComplete"] == 100:
313            server_name = queue["Servers"][0]
314            creds = get_creds()
315            clc.v2.SetCredentials(creds["user"], creds["password"])
316            ip_addresses = clc.v2.Server(server_name).ip_addresses
317            internal_ip_address = ip_addresses[0]["internal"]
318            return internal_ip_address
319        else:
320            counter = counter + 1
321            log.info(
322                "Creating Cloud VM %s Time out in %s minutes",
323                nodename,
324                str(10 - counter),
325            )
326            time.sleep(60)
327
328
329def create(vm_):
330    """
331    get the system build going
332    """
333    creds = get_creds()
334    clc.v1.SetCredentials(creds["token"], creds["token_pass"])
335    cloud_profile = config.is_provider_configured(
336        __opts__, _get_active_provider_name() or __virtualname__, ("token",)
337    )
338    group = config.get_cloud_config_value(
339        "group",
340        vm_,
341        __opts__,
342        search_global=False,
343        default=None,
344    )
345    name = vm_["name"]
346    description = config.get_cloud_config_value(
347        "description",
348        vm_,
349        __opts__,
350        search_global=False,
351        default=None,
352    )
353    ram = config.get_cloud_config_value(
354        "ram",
355        vm_,
356        __opts__,
357        search_global=False,
358        default=None,
359    )
360    backup_level = config.get_cloud_config_value(
361        "backup_level",
362        vm_,
363        __opts__,
364        search_global=False,
365        default=None,
366    )
367    template = config.get_cloud_config_value(
368        "template",
369        vm_,
370        __opts__,
371        search_global=False,
372        default=None,
373    )
374    password = config.get_cloud_config_value(
375        "password",
376        vm_,
377        __opts__,
378        search_global=False,
379        default=None,
380    )
381    cpu = config.get_cloud_config_value(
382        "cpu",
383        vm_,
384        __opts__,
385        search_global=False,
386        default=None,
387    )
388    network = config.get_cloud_config_value(
389        "network",
390        vm_,
391        __opts__,
392        search_global=False,
393        default=None,
394    )
395    location = config.get_cloud_config_value(
396        "location",
397        vm_,
398        __opts__,
399        search_global=False,
400        default=None,
401    )
402    if len(name) > 6:
403        name = name[0:6]
404    if len(password) < 9:
405        password = ""
406    clc_return = clc.v1.Server.Create(
407        alias=None,
408        location=(location),
409        name=(name),
410        template=(template),
411        cpu=(cpu),
412        ram=(ram),
413        backup_level=(backup_level),
414        group=(group),
415        network=(network),
416        description=(description),
417        password=(password),
418    )
419    req_id = clc_return["RequestID"]
420    vm_["ssh_host"] = get_build_status(req_id, name)
421    __utils__["cloud.fire_event"](
422        "event",
423        "waiting for ssh",
424        "salt/cloud/{}/waiting_for_ssh".format(name),
425        sock_dir=__opts__["sock_dir"],
426        args={"ip_address": vm_["ssh_host"]},
427        transport=__opts__["transport"],
428    )
429
430    # Bootstrap!
431    ret = __utils__["cloud.bootstrap"](vm_, __opts__)
432    return_message = {"Server Name": name, "IP Address": vm_["ssh_host"]}
433    ret.update(return_message)
434    return return_message
435
436
437def destroy(name, call=None):
438    """
439    destroy the vm
440    """
441    return {"status": "destroying must be done via https://control.ctl.io at this time"}
442