1""" 2Joyent Cloud Module 3=================== 4 5The Joyent Cloud module is used to interact with the Joyent cloud system. 6 7Set up the cloud configuration at ``/etc/salt/cloud.providers`` or 8``/etc/salt/cloud.providers.d/joyent.conf``: 9 10.. code-block:: yaml 11 12 my-joyent-config: 13 driver: joyent 14 # The Joyent login user 15 user: fred 16 # The Joyent user's password 17 password: saltybacon 18 # The location of the ssh private key that can log into the new VM 19 private_key: /root/mykey.pem 20 # The name of the private key 21 keyname: mykey 22 23When creating your profiles for the joyent cloud, add the location attribute to 24the profile, this will automatically get picked up when performing tasks 25associated with that vm. An example profile might look like: 26 27.. code-block:: yaml 28 29 joyent_512: 30 provider: my-joyent-config 31 size: g4-highcpu-512M 32 image: centos-6 33 location: us-east-1 34 35This driver can also be used with the Joyent SmartDataCenter project. More 36details can be found at: 37 38.. _`SmartDataCenter`: https://github.com/joyent/sdc 39 40Using SDC requires that an api_host_suffix is set. The default value for this is 41`.api.joyentcloud.com`. All characters, including the leading `.`, should be 42included: 43 44.. code-block:: yaml 45 46 api_host_suffix: .api.myhostname.com 47 48:depends: PyCrypto 49""" 50 51import base64 52import datetime 53import http.client 54import inspect 55import logging 56import os 57import pprint 58 59import salt.config as config 60import salt.utils.cloud 61import salt.utils.files 62import salt.utils.http 63import salt.utils.json 64import salt.utils.yaml 65from salt.exceptions import ( 66 SaltCloudExecutionFailure, 67 SaltCloudExecutionTimeout, 68 SaltCloudNotFound, 69 SaltCloudSystemExit, 70) 71 72try: 73 from M2Crypto import EVP 74 75 HAS_REQUIRED_CRYPTO = True 76 HAS_M2 = True 77except ImportError: 78 HAS_M2 = False 79 try: 80 from Cryptodome.Hash import SHA256 81 from Cryptodome.Signature import PKCS1_v1_5 82 83 HAS_REQUIRED_CRYPTO = True 84 except ImportError: 85 try: 86 from Crypto.Hash import SHA256 # nosec 87 from Crypto.Signature import PKCS1_v1_5 # nosec 88 89 HAS_REQUIRED_CRYPTO = True 90 except ImportError: 91 HAS_REQUIRED_CRYPTO = False 92 93 94# Get logging started 95log = logging.getLogger(__name__) 96 97__virtualname__ = "joyent" 98 99JOYENT_API_HOST_SUFFIX = ".api.joyentcloud.com" 100JOYENT_API_VERSION = "~7.2" 101 102JOYENT_LOCATIONS = { 103 "us-east-1": "North Virginia, USA", 104 "us-west-1": "Bay Area, California, USA", 105 "us-sw-1": "Las Vegas, Nevada, USA", 106 "eu-ams-1": "Amsterdam, Netherlands", 107} 108DEFAULT_LOCATION = "us-east-1" 109 110# joyent no longer reports on all data centers, so setting this value to true 111# causes the list_nodes function to get information on machines from all 112# data centers 113POLL_ALL_LOCATIONS = True 114 115VALID_RESPONSE_CODES = [ 116 http.client.OK, 117 http.client.ACCEPTED, 118 http.client.CREATED, 119 http.client.NO_CONTENT, 120] 121 122 123# Only load in this module if the Joyent configurations are in place 124def __virtual__(): 125 """ 126 Check for Joyent configs 127 """ 128 if HAS_REQUIRED_CRYPTO is False: 129 return False, "Either PyCrypto or Cryptodome needs to be installed." 130 if get_configured_provider() is False: 131 return False 132 133 return __virtualname__ 134 135 136def _get_active_provider_name(): 137 try: 138 return __active_provider_name__.value() 139 except AttributeError: 140 return __active_provider_name__ 141 142 143def get_configured_provider(): 144 """ 145 Return the first configured instance. 146 """ 147 return config.is_provider_configured( 148 __opts__, _get_active_provider_name() or __virtualname__, ("user", "password") 149 ) 150 151 152def get_image(vm_): 153 """ 154 Return the image object to use 155 """ 156 images = avail_images() 157 158 vm_image = config.get_cloud_config_value("image", vm_, __opts__) 159 160 if vm_image and str(vm_image) in images: 161 images[vm_image]["name"] = images[vm_image]["id"] 162 return images[vm_image] 163 164 raise SaltCloudNotFound( 165 "The specified image, '{}', could not be found.".format(vm_image) 166 ) 167 168 169def get_size(vm_): 170 """ 171 Return the VM's size object 172 """ 173 sizes = avail_sizes() 174 vm_size = config.get_cloud_config_value("size", vm_, __opts__) 175 if not vm_size: 176 raise SaltCloudNotFound("No size specified for this VM.") 177 178 if vm_size and str(vm_size) in sizes: 179 return sizes[vm_size] 180 181 raise SaltCloudNotFound( 182 "The specified size, '{}', could not be found.".format(vm_size) 183 ) 184 185 186def query_instance(vm_=None, call=None): 187 """ 188 Query an instance upon creation from the Joyent API 189 """ 190 if isinstance(vm_, str) and call == "action": 191 vm_ = {"name": vm_, "provider": "joyent"} 192 193 if call == "function": 194 # Technically this function may be called other ways too, but it 195 # definitely cannot be called with --function. 196 raise SaltCloudSystemExit( 197 "The query_instance action must be called with -a or --action." 198 ) 199 200 __utils__["cloud.fire_event"]( 201 "event", 202 "querying instance", 203 "salt/cloud/{}/querying".format(vm_["name"]), 204 sock_dir=__opts__["sock_dir"], 205 transport=__opts__["transport"], 206 ) 207 208 def _query_ip_address(): 209 data = show_instance(vm_["name"], call="action") 210 if not data: 211 log.error("There was an error while querying Joyent. Empty response") 212 # Trigger a failure in the wait for IP function 213 return False 214 215 if isinstance(data, dict) and "error" in data: 216 log.warning("There was an error in the query %s", data.get("error")) 217 # Trigger a failure in the wait for IP function 218 return False 219 220 log.debug("Returned query data: %s", data) 221 222 if "primaryIp" in data[1]: 223 # Wait for SSH to be fully configured on the remote side 224 if data[1]["state"] == "running": 225 return data[1]["primaryIp"] 226 return None 227 228 try: 229 data = salt.utils.cloud.wait_for_ip( 230 _query_ip_address, 231 timeout=config.get_cloud_config_value( 232 "wait_for_ip_timeout", vm_, __opts__, default=10 * 60 233 ), 234 interval=config.get_cloud_config_value( 235 "wait_for_ip_interval", vm_, __opts__, default=10 236 ), 237 interval_multiplier=config.get_cloud_config_value( 238 "wait_for_ip_interval_multiplier", vm_, __opts__, default=1 239 ), 240 ) 241 except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc: 242 try: 243 # destroy(vm_['name']) 244 pass 245 except SaltCloudSystemExit: 246 pass 247 finally: 248 raise SaltCloudSystemExit(str(exc)) 249 250 return data 251 252 253def create(vm_): 254 """ 255 Create a single VM from a data dict 256 257 CLI Example: 258 259 .. code-block:: bash 260 261 salt-cloud -p profile_name vm_name 262 """ 263 try: 264 # Check for required profile parameters before sending any API calls. 265 if ( 266 vm_["profile"] 267 and config.is_profile_configured( 268 __opts__, 269 _get_active_provider_name() or "joyent", 270 vm_["profile"], 271 vm_=vm_, 272 ) 273 is False 274 ): 275 return False 276 except AttributeError: 277 pass 278 279 key_filename = config.get_cloud_config_value( 280 "private_key", vm_, __opts__, search_global=False, default=None 281 ) 282 283 __utils__["cloud.fire_event"]( 284 "event", 285 "starting create", 286 "salt/cloud/{}/creating".format(vm_["name"]), 287 args=__utils__["cloud.filter_event"]( 288 "creating", vm_, ["name", "profile", "provider", "driver"] 289 ), 290 sock_dir=__opts__["sock_dir"], 291 transport=__opts__["transport"], 292 ) 293 294 log.info( 295 "Creating Cloud VM %s in %s", vm_["name"], vm_.get("location", DEFAULT_LOCATION) 296 ) 297 298 # added . for fqdn hostnames 299 salt.utils.cloud.check_name(vm_["name"], "a-zA-Z0-9-.") 300 kwargs = { 301 "name": vm_["name"], 302 "image": get_image(vm_), 303 "size": get_size(vm_), 304 "location": vm_.get("location", DEFAULT_LOCATION), 305 } 306 # Let's not assign a default here; only assign a network value if 307 # one is explicitly configured 308 if "networks" in vm_: 309 kwargs["networks"] = vm_.get("networks") 310 311 __utils__["cloud.fire_event"]( 312 "event", 313 "requesting instance", 314 "salt/cloud/{}/requesting".format(vm_["name"]), 315 args={ 316 "kwargs": __utils__["cloud.filter_event"]( 317 "requesting", kwargs, list(kwargs) 318 ), 319 }, 320 sock_dir=__opts__["sock_dir"], 321 transport=__opts__["transport"], 322 ) 323 324 data = create_node(**kwargs) 325 if data == {}: 326 log.error("Error creating %s on JOYENT", vm_["name"]) 327 return False 328 329 query_instance(vm_) 330 data = show_instance(vm_["name"], call="action") 331 332 vm_["key_filename"] = key_filename 333 vm_["ssh_host"] = data[1]["primaryIp"] 334 335 __utils__["cloud.bootstrap"](vm_, __opts__) 336 337 __utils__["cloud.fire_event"]( 338 "event", 339 "created instance", 340 "salt/cloud/{}/created".format(vm_["name"]), 341 args=__utils__["cloud.filter_event"]( 342 "created", vm_, ["name", "profile", "provider", "driver"] 343 ), 344 sock_dir=__opts__["sock_dir"], 345 transport=__opts__["transport"], 346 ) 347 348 return data[1] 349 350 351def create_node(**kwargs): 352 """ 353 convenience function to make the rest api call for node creation. 354 """ 355 name = kwargs["name"] 356 size = kwargs["size"] 357 image = kwargs["image"] 358 location = kwargs["location"] 359 networks = kwargs.get("networks") 360 tag = kwargs.get("tag") 361 locality = kwargs.get("locality") 362 metadata = kwargs.get("metadata") 363 firewall_enabled = kwargs.get("firewall_enabled") 364 365 create_data = { 366 "name": name, 367 "package": size["name"], 368 "image": image["name"], 369 } 370 if networks is not None: 371 create_data["networks"] = networks 372 373 if locality is not None: 374 create_data["locality"] = locality 375 376 if metadata is not None: 377 for key, value in metadata.items(): 378 create_data["metadata.{}".format(key)] = value 379 380 if tag is not None: 381 for key, value in tag.items(): 382 create_data["tag.{}".format(key)] = value 383 384 if firewall_enabled is not None: 385 create_data["firewall_enabled"] = firewall_enabled 386 387 data = salt.utils.json.dumps(create_data) 388 389 ret = query(command="my/machines", data=data, method="POST", location=location) 390 if ret[0] in VALID_RESPONSE_CODES: 391 return ret[1] 392 else: 393 log.error("Failed to create node %s: %s", name, ret[1]) 394 395 return {} 396 397 398def destroy(name, call=None): 399 """ 400 destroy a machine by name 401 402 :param name: name given to the machine 403 :param call: call value in this case is 'action' 404 :return: array of booleans , true if successfully stopped and true if 405 successfully removed 406 407 CLI Example: 408 409 .. code-block:: bash 410 411 salt-cloud -d vm_name 412 413 """ 414 if call == "function": 415 raise SaltCloudSystemExit( 416 "The destroy action must be called with -d, --destroy, -a or --action." 417 ) 418 419 __utils__["cloud.fire_event"]( 420 "event", 421 "destroying instance", 422 "salt/cloud/{}/destroying".format(name), 423 args={"name": name}, 424 sock_dir=__opts__["sock_dir"], 425 transport=__opts__["transport"], 426 ) 427 428 node = get_node(name) 429 ret = query( 430 command="my/machines/{}".format(node["id"]), 431 location=node["location"], 432 method="DELETE", 433 ) 434 435 __utils__["cloud.fire_event"]( 436 "event", 437 "destroyed instance", 438 "salt/cloud/{}/destroyed".format(name), 439 args={"name": name}, 440 sock_dir=__opts__["sock_dir"], 441 transport=__opts__["transport"], 442 ) 443 444 if __opts__.get("update_cachedir", False) is True: 445 __utils__["cloud.delete_minion_cachedir"]( 446 name, _get_active_provider_name().split(":")[0], __opts__ 447 ) 448 449 return ret[0] in VALID_RESPONSE_CODES 450 451 452def reboot(name, call=None): 453 """ 454 reboot a machine by name 455 :param name: name given to the machine 456 :param call: call value in this case is 'action' 457 :return: true if successful 458 459 CLI Example: 460 461 .. code-block:: bash 462 463 salt-cloud -a reboot vm_name 464 """ 465 node = get_node(name) 466 ret = take_action( 467 name=name, 468 call=call, 469 method="POST", 470 command="my/machines/{}".format(node["id"]), 471 location=node["location"], 472 data={"action": "reboot"}, 473 ) 474 return ret[0] in VALID_RESPONSE_CODES 475 476 477def stop(name, call=None): 478 """ 479 stop a machine by name 480 :param name: name given to the machine 481 :param call: call value in this case is 'action' 482 :return: true if successful 483 484 CLI Example: 485 486 .. code-block:: bash 487 488 salt-cloud -a stop vm_name 489 """ 490 node = get_node(name) 491 ret = take_action( 492 name=name, 493 call=call, 494 method="POST", 495 command="my/machines/{}".format(node["id"]), 496 location=node["location"], 497 data={"action": "stop"}, 498 ) 499 return ret[0] in VALID_RESPONSE_CODES 500 501 502def start(name, call=None): 503 """ 504 start a machine by name 505 :param name: name given to the machine 506 :param call: call value in this case is 'action' 507 :return: true if successful 508 509 CLI Example: 510 511 .. code-block:: bash 512 513 salt-cloud -a start vm_name 514 """ 515 node = get_node(name) 516 ret = take_action( 517 name=name, 518 call=call, 519 method="POST", 520 command="my/machines/{}".format(node["id"]), 521 location=node["location"], 522 data={"action": "start"}, 523 ) 524 return ret[0] in VALID_RESPONSE_CODES 525 526 527def take_action( 528 name=None, 529 call=None, 530 command=None, 531 data=None, 532 method="GET", 533 location=DEFAULT_LOCATION, 534): 535 536 """ 537 take action call used by start,stop, reboot 538 :param name: name given to the machine 539 :param call: call value in this case is 'action' 540 :command: api path 541 :data: any data to be passed to the api, must be in json format 542 :method: GET,POST,or DELETE 543 :location: data center to execute the command on 544 :return: true if successful 545 """ 546 caller = inspect.stack()[1][3] 547 548 if call != "action": 549 raise SaltCloudSystemExit("This action must be called with -a or --action.") 550 551 if data: 552 data = salt.utils.json.dumps(data) 553 554 ret = [] 555 try: 556 557 ret = query(command=command, data=data, method=method, location=location) 558 log.info("Success %s for node %s", caller, name) 559 except Exception as exc: # pylint: disable=broad-except 560 if "InvalidState" in str(exc): 561 ret = [200, {}] 562 else: 563 log.error( 564 "Failed to invoke %s node %s: %s", 565 caller, 566 name, 567 exc, 568 # Show the traceback if the debug logging level is enabled 569 exc_info_on_loglevel=logging.DEBUG, 570 ) 571 ret = [100, {}] 572 573 return ret 574 575 576def ssh_interface(vm_): 577 """ 578 Return the ssh_interface type to connect to. Either 'public_ips' (default) 579 or 'private_ips'. 580 """ 581 return config.get_cloud_config_value( 582 "ssh_interface", vm_, __opts__, default="public_ips", search_global=False 583 ) 584 585 586def get_location(vm_=None): 587 """ 588 Return the joyent data center to use, in this order: 589 - CLI parameter 590 - VM parameter 591 - Cloud profile setting 592 """ 593 return __opts__.get( 594 "location", 595 config.get_cloud_config_value( 596 "location", 597 vm_ or get_configured_provider(), 598 __opts__, 599 default=DEFAULT_LOCATION, 600 search_global=False, 601 ), 602 ) 603 604 605def avail_locations(call=None): 606 """ 607 List all available locations 608 """ 609 if call == "action": 610 raise SaltCloudSystemExit( 611 "The avail_locations function must be called with " 612 "-f or --function, or with the --list-locations option" 613 ) 614 615 ret = {} 616 for key in JOYENT_LOCATIONS: 617 ret[key] = {"name": key, "region": JOYENT_LOCATIONS[key]} 618 619 # this can be enabled when the bug in the joyent get data centers call is 620 # corrected, currently only the European dc (new api) returns the correct 621 # values 622 # ret = {} 623 # rcode, datacenters = query( 624 # command='my/datacenters', location=DEFAULT_LOCATION, method='GET' 625 # ) 626 # if rcode in VALID_RESPONSE_CODES and isinstance(datacenters, dict): 627 # for key in datacenters: 628 # ret[key] = { 629 # 'name': key, 630 # 'url': datacenters[key] 631 # } 632 return ret 633 634 635def has_method(obj, method_name): 636 """ 637 Find if the provided object has a specific method 638 """ 639 if method_name in dir(obj): 640 return True 641 642 log.error("Method '%s' not yet supported!", method_name) 643 return False 644 645 646def key_list(items=None): 647 """ 648 convert list to dictionary using the key as the identifier 649 :param items: array to iterate over 650 :return: dictionary 651 """ 652 if items is None: 653 items = [] 654 655 ret = {} 656 if items and isinstance(items, list): 657 for item in items: 658 if "name" in item: 659 # added for consistency with old code 660 if "id" not in item: 661 item["id"] = item["name"] 662 ret[item["name"]] = item 663 return ret 664 665 666def get_node(name): 667 """ 668 gets the node from the full node list by name 669 :param name: name of the vm 670 :return: node object 671 """ 672 nodes = list_nodes() 673 if name in nodes: 674 return nodes[name] 675 return None 676 677 678def show_instance(name, call=None): 679 """ 680 get details about a machine 681 :param name: name given to the machine 682 :param call: call value in this case is 'action' 683 :return: machine information 684 685 CLI Example: 686 687 .. code-block:: bash 688 689 salt-cloud -a show_instance vm_name 690 """ 691 node = get_node(name) 692 ret = query( 693 command="my/machines/{}".format(node["id"]), 694 location=node["location"], 695 method="GET", 696 ) 697 698 return ret 699 700 701def _old_libcloud_node_state(id_): 702 """ 703 Libcloud supported node states 704 """ 705 states_int = { 706 0: "RUNNING", 707 1: "REBOOTING", 708 2: "TERMINATED", 709 3: "PENDING", 710 4: "UNKNOWN", 711 5: "STOPPED", 712 6: "SUSPENDED", 713 7: "ERROR", 714 8: "PAUSED", 715 } 716 states_str = { 717 "running": "RUNNING", 718 "rebooting": "REBOOTING", 719 "starting": "STARTING", 720 "terminated": "TERMINATED", 721 "pending": "PENDING", 722 "unknown": "UNKNOWN", 723 "stopping": "STOPPING", 724 "stopped": "STOPPED", 725 "suspended": "SUSPENDED", 726 "error": "ERROR", 727 "paused": "PAUSED", 728 "reconfiguring": "RECONFIGURING", 729 } 730 return states_str[id_] if isinstance(id_, str) else states_int[id_] 731 732 733def joyent_node_state(id_): 734 """ 735 Convert joyent returned state to state common to other data center return 736 values for consistency 737 738 :param id_: joyent state value 739 :return: state value 740 """ 741 states = { 742 "running": 0, 743 "stopped": 2, 744 "stopping": 2, 745 "provisioning": 3, 746 "deleted": 2, 747 "unknown": 4, 748 } 749 750 if id_ not in states: 751 id_ = "unknown" 752 753 return _old_libcloud_node_state(states[id_]) 754 755 756def reformat_node(item=None, full=False): 757 """ 758 Reformat the returned data from joyent, determine public/private IPs and 759 strip out fields if necessary to provide either full or brief content. 760 761 :param item: node dictionary 762 :param full: full or brief output 763 :return: dict 764 """ 765 desired_keys = [ 766 "id", 767 "name", 768 "state", 769 "public_ips", 770 "private_ips", 771 "size", 772 "image", 773 "location", 774 ] 775 item["private_ips"] = [] 776 item["public_ips"] = [] 777 if "ips" in item: 778 for ip in item["ips"]: 779 if salt.utils.cloud.is_public_ip(ip): 780 item["public_ips"].append(ip) 781 else: 782 item["private_ips"].append(ip) 783 784 # add any undefined desired keys 785 for key in desired_keys: 786 if key not in item: 787 item[key] = None 788 789 # remove all the extra key value pairs to provide a brief listing 790 to_del = [] 791 if not full: 792 for key in item.keys(): # iterate over a copy of the keys 793 if key not in desired_keys: 794 to_del.append(key) 795 796 for key in to_del: 797 del item[key] 798 799 if "state" in item: 800 item["state"] = joyent_node_state(item["state"]) 801 802 return item 803 804 805def list_nodes(full=False, call=None): 806 """ 807 list of nodes, keeping only a brief listing 808 809 CLI Example: 810 811 .. code-block:: bash 812 813 salt-cloud -Q 814 """ 815 if call == "action": 816 raise SaltCloudSystemExit( 817 "The list_nodes function must be called with -f or --function." 818 ) 819 820 ret = {} 821 if POLL_ALL_LOCATIONS: 822 for location in JOYENT_LOCATIONS: 823 result = query(command="my/machines", location=location, method="GET") 824 if result[0] in VALID_RESPONSE_CODES: 825 nodes = result[1] 826 for node in nodes: 827 if "name" in node: 828 node["location"] = location 829 ret[node["name"]] = reformat_node(item=node, full=full) 830 else: 831 log.error("Invalid response when listing Joyent nodes: %s", result[1]) 832 833 else: 834 location = get_location() 835 result = query(command="my/machines", location=location, method="GET") 836 nodes = result[1] 837 for node in nodes: 838 if "name" in node: 839 node["location"] = location 840 ret[node["name"]] = reformat_node(item=node, full=full) 841 return ret 842 843 844def list_nodes_full(call=None): 845 """ 846 list of nodes, maintaining all content provided from joyent listings 847 848 CLI Example: 849 850 .. code-block:: bash 851 852 salt-cloud -F 853 """ 854 if call == "action": 855 raise SaltCloudSystemExit( 856 "The list_nodes_full function must be called with -f or --function." 857 ) 858 859 return list_nodes(full=True) 860 861 862def list_nodes_select(call=None): 863 """ 864 Return a list of the VMs that are on the provider, with select fields 865 """ 866 return salt.utils.cloud.list_nodes_select( 867 list_nodes_full("function"), 868 __opts__["query.selection"], 869 call, 870 ) 871 872 873def _get_proto(): 874 """ 875 Checks configuration to see whether the user has SSL turned on. Default is: 876 877 .. code-block:: yaml 878 879 use_ssl: True 880 """ 881 use_ssl = config.get_cloud_config_value( 882 "use_ssl", 883 get_configured_provider(), 884 __opts__, 885 search_global=False, 886 default=True, 887 ) 888 if use_ssl is True: 889 return "https" 890 return "http" 891 892 893def avail_images(call=None): 894 """ 895 Get list of available images 896 897 CLI Example: 898 899 .. code-block:: bash 900 901 salt-cloud --list-images 902 903 Can use a custom URL for images. Default is: 904 905 .. code-block:: yaml 906 907 image_url: images.joyent.com/images 908 """ 909 if call == "action": 910 raise SaltCloudSystemExit( 911 "The avail_images function must be called with " 912 "-f or --function, or with the --list-images option" 913 ) 914 915 user = config.get_cloud_config_value( 916 "user", get_configured_provider(), __opts__, search_global=False 917 ) 918 919 img_url = config.get_cloud_config_value( 920 "image_url", 921 get_configured_provider(), 922 __opts__, 923 search_global=False, 924 default="{}{}/{}/images".format(DEFAULT_LOCATION, JOYENT_API_HOST_SUFFIX, user), 925 ) 926 927 if not img_url.startswith("http://") and not img_url.startswith("https://"): 928 img_url = "{}://{}".format(_get_proto(), img_url) 929 930 rcode, data = query(command="my/images", method="GET") 931 log.debug(data) 932 933 ret = {} 934 for image in data: 935 ret[image["name"]] = image 936 return ret 937 938 939def avail_sizes(call=None): 940 """ 941 get list of available packages 942 943 CLI Example: 944 945 .. code-block:: bash 946 947 salt-cloud --list-sizes 948 """ 949 if call == "action": 950 raise SaltCloudSystemExit( 951 "The avail_sizes function must be called with " 952 "-f or --function, or with the --list-sizes option" 953 ) 954 955 rcode, items = query(command="my/packages") 956 if rcode not in VALID_RESPONSE_CODES: 957 return {} 958 return key_list(items=items) 959 960 961def list_keys(kwargs=None, call=None): 962 """ 963 List the keys available 964 """ 965 if call != "function": 966 log.error("The list_keys function must be called with -f or --function.") 967 return False 968 969 if not kwargs: 970 kwargs = {} 971 972 ret = {} 973 rcode, data = query(command="my/keys", method="GET") 974 for pair in data: 975 ret[pair["name"]] = pair["key"] 976 return {"keys": ret} 977 978 979def show_key(kwargs=None, call=None): 980 """ 981 List the keys available 982 """ 983 if call != "function": 984 log.error("The list_keys function must be called with -f or --function.") 985 return False 986 987 if not kwargs: 988 kwargs = {} 989 990 if "keyname" not in kwargs: 991 log.error("A keyname is required.") 992 return False 993 994 rcode, data = query( 995 command="my/keys/{}".format(kwargs["keyname"]), 996 method="GET", 997 ) 998 return {"keys": {data["name"]: data["key"]}} 999 1000 1001def import_key(kwargs=None, call=None): 1002 """ 1003 List the keys available 1004 1005 CLI Example: 1006 1007 .. code-block:: bash 1008 1009 salt-cloud -f import_key joyent keyname=mykey keyfile=/tmp/mykey.pub 1010 """ 1011 if call != "function": 1012 log.error("The import_key function must be called with -f or --function.") 1013 return False 1014 1015 if not kwargs: 1016 kwargs = {} 1017 1018 if "keyname" not in kwargs: 1019 log.error("A keyname is required.") 1020 return False 1021 1022 if "keyfile" not in kwargs: 1023 log.error("The location of the SSH keyfile is required.") 1024 return False 1025 1026 if not os.path.isfile(kwargs["keyfile"]): 1027 log.error("The specified keyfile (%s) does not exist.", kwargs["keyfile"]) 1028 return False 1029 1030 with salt.utils.files.fopen(kwargs["keyfile"], "r") as fp_: 1031 kwargs["key"] = salt.utils.stringutils.to_unicode(fp_.read()) 1032 1033 send_data = {"name": kwargs["keyname"], "key": kwargs["key"]} 1034 kwargs["data"] = salt.utils.json.dumps(send_data) 1035 1036 rcode, data = query( 1037 command="my/keys", 1038 method="POST", 1039 data=kwargs["data"], 1040 ) 1041 log.debug(pprint.pformat(data)) 1042 return {"keys": {data["name"]: data["key"]}} 1043 1044 1045def delete_key(kwargs=None, call=None): 1046 """ 1047 List the keys available 1048 1049 CLI Example: 1050 1051 .. code-block:: bash 1052 1053 salt-cloud -f delete_key joyent keyname=mykey 1054 """ 1055 if call != "function": 1056 log.error("The delete_keys function must be called with -f or --function.") 1057 return False 1058 1059 if not kwargs: 1060 kwargs = {} 1061 1062 if "keyname" not in kwargs: 1063 log.error("A keyname is required.") 1064 return False 1065 1066 rcode, data = query( 1067 command="my/keys/{}".format(kwargs["keyname"]), 1068 method="DELETE", 1069 ) 1070 return data 1071 1072 1073def get_location_path( 1074 location=DEFAULT_LOCATION, api_host_suffix=JOYENT_API_HOST_SUFFIX 1075): 1076 """ 1077 create url from location variable 1078 :param location: joyent data center location 1079 :return: url 1080 """ 1081 return "{}://{}{}".format(_get_proto(), location, api_host_suffix) 1082 1083 1084def query(action=None, command=None, args=None, method="GET", location=None, data=None): 1085 """ 1086 Make a web call to Joyent 1087 """ 1088 user = config.get_cloud_config_value( 1089 "user", get_configured_provider(), __opts__, search_global=False 1090 ) 1091 1092 if not user: 1093 log.error( 1094 "username is required for Joyent API requests. Please set one in your" 1095 " provider configuration" 1096 ) 1097 1098 password = config.get_cloud_config_value( 1099 "password", get_configured_provider(), __opts__, search_global=False 1100 ) 1101 1102 verify_ssl = config.get_cloud_config_value( 1103 "verify_ssl", 1104 get_configured_provider(), 1105 __opts__, 1106 search_global=False, 1107 default=True, 1108 ) 1109 1110 ssh_keyfile = config.get_cloud_config_value( 1111 "private_key", 1112 get_configured_provider(), 1113 __opts__, 1114 search_global=False, 1115 default=True, 1116 ) 1117 1118 if not ssh_keyfile: 1119 log.error( 1120 "ssh_keyfile is required for Joyent API requests. Please set one in your" 1121 " provider configuration" 1122 ) 1123 1124 ssh_keyname = config.get_cloud_config_value( 1125 "keyname", 1126 get_configured_provider(), 1127 __opts__, 1128 search_global=False, 1129 default=True, 1130 ) 1131 1132 if not ssh_keyname: 1133 log.error( 1134 "ssh_keyname is required for Joyent API requests. Please set one in your" 1135 " provider configuration" 1136 ) 1137 1138 if not location: 1139 location = get_location() 1140 1141 api_host_suffix = config.get_cloud_config_value( 1142 "api_host_suffix", 1143 get_configured_provider(), 1144 __opts__, 1145 search_global=False, 1146 default=JOYENT_API_HOST_SUFFIX, 1147 ) 1148 1149 path = get_location_path(location=location, api_host_suffix=api_host_suffix) 1150 1151 if action: 1152 path += action 1153 1154 if command: 1155 path += "/{}".format(command) 1156 1157 log.debug("User: '%s' on PATH: %s", user, path) 1158 1159 if (not user) or (not ssh_keyfile) or (not ssh_keyname) or (not location): 1160 return None 1161 1162 timenow = datetime.datetime.utcnow() 1163 timestamp = timenow.strftime("%a, %d %b %Y %H:%M:%S %Z").strip() 1164 rsa_key = salt.crypt.get_rsa_key(ssh_keyfile, None) 1165 if HAS_M2: 1166 md = EVP.MessageDigest("sha256") 1167 md.update(timestamp.encode(__salt_system_encoding__)) 1168 digest = md.final() 1169 signed = rsa_key.sign(digest, algo="sha256") 1170 else: 1171 rsa_ = PKCS1_v1_5.new(rsa_key) 1172 hash_ = SHA256.new() 1173 hash_.update(timestamp.encode(__salt_system_encoding__)) 1174 signed = rsa_.sign(hash_) 1175 signed = base64.b64encode(signed) 1176 user_arr = user.split("/") 1177 if len(user_arr) == 1: 1178 keyid = "/{}/keys/{}".format(user_arr[0], ssh_keyname) 1179 elif len(user_arr) == 2: 1180 keyid = "/{}/users/{}/keys/{}".format(user_arr[0], user_arr[1], ssh_keyname) 1181 else: 1182 log.error("Malformed user string") 1183 1184 headers = { 1185 "Content-Type": "application/json", 1186 "Accept": "application/json", 1187 "X-Api-Version": JOYENT_API_VERSION, 1188 "Date": timestamp, 1189 "Authorization": 'Signature keyId="{}",algorithm="rsa-sha256" {}'.format( 1190 keyid, signed.decode(__salt_system_encoding__) 1191 ), 1192 } 1193 1194 if not isinstance(args, dict): 1195 args = {} 1196 1197 # post form data 1198 if not data: 1199 data = salt.utils.json.dumps({}) 1200 1201 return_content = None 1202 result = salt.utils.http.query( 1203 path, 1204 method, 1205 params=args, 1206 header_dict=headers, 1207 data=data, 1208 decode=False, 1209 text=True, 1210 status=True, 1211 headers=True, 1212 verify_ssl=verify_ssl, 1213 opts=__opts__, 1214 ) 1215 log.debug("Joyent Response Status Code: %s", result["status"]) 1216 if "headers" not in result: 1217 return [result["status"], result["error"]] 1218 1219 if "Content-Length" in result["headers"]: 1220 content = result["text"] 1221 return_content = salt.utils.yaml.safe_load(content) 1222 1223 return [result["status"], return_content] 1224