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