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