From 93d6569ff10202b76bf8f7507bf4d007d2bdaca8 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 25 Jun 2026 16:29:39 -0700 Subject: [PATCH 1/7] Document every salt-cloud event tag in one reference page --- changelog/64629.fixed.md | 1 + doc/topics/cloud/events.rst | 325 ++++++++++++++++++ doc/topics/cloud/index.rst | 7 + doc/topics/cloud/reactor.rst | 19 +- doc/topics/event/master_events.rst | 4 + .../cloud/test_cloud_events_documented.py | 188 ++++++++++ 6 files changed, 541 insertions(+), 3 deletions(-) create mode 100644 changelog/64629.fixed.md create mode 100644 doc/topics/cloud/events.rst create mode 100644 tests/pytests/unit/cloud/test_cloud_events_documented.py diff --git a/changelog/64629.fixed.md b/changelog/64629.fixed.md new file mode 100644 index 000000000000..d4b1e0c33efb --- /dev/null +++ b/changelog/64629.fixed.md @@ -0,0 +1 @@ +Documented every event tag emitted by Salt Cloud on a new reference page (``doc/topics/cloud/events.rst``). The lifecycle, power-state, reactor-hook, and per-resource (disk, snapshot, network, address, firewall, load balancer, health check, spot request, block volume) tags are now described in one place; the Salt Master Events page and the reactor tutorial cross-link to it. Resolves the inconsistency between the two pre-existing event lists noted in issues #64629 and #65063. diff --git a/doc/topics/cloud/events.rst b/doc/topics/cloud/events.rst new file mode 100644 index 000000000000..f1b292732ac2 --- /dev/null +++ b/doc/topics/cloud/events.rst @@ -0,0 +1,325 @@ +.. _cloud-events-reference: + +=========================== +Salt Cloud Events Reference +=========================== + +This page is the canonical reference for the events Salt Cloud fires on the +Salt event bus. Reactors can subscribe to any of these tags to take action +when an instance is created, destroyed, resized, or otherwise acted upon. + +Event tags follow the pattern:: + + salt/cloud// + +where ```` is normally the VM name, but may also be the name of a +shared resource (a disk, a load balancer, a snapshot, an IP address, etc.) +when the event describes a non-VM operation. + +For background on using these tags with the Salt Reactor system, see +:ref:`salt-cloud-with-reactor`. For the general Salt event bus event reference, +see :ref:`event-master_events`. + +Lifecycle Events +================ + +These tags are fired during the normal create / destroy lifecycle of a VM and +are emitted by the majority of cloud drivers. + +.. salt:event:: salt/cloud//creating + + Fired when ``salt-cloud`` begins the VM creation process. No work has + been done against the cloud provider yet. + + :var name: name of the VM being created. + :var profile: the cloud profile selected for the VM. + :var provider: the cloud provider configured for that profile. + :var driver: the cloud driver name (e.g. ``ec2``, ``vmware``). + +.. salt:event:: salt/cloud//requesting + + Fired immediately before the create-VM request is sent to the cloud + provider. The payload contains the kwargs that will be passed to the + provider API, with passwords, keys, and other sensitive values stripped. + + :var kwargs: dictionary of provider-specific create parameters. Common + keys include ``name``, ``image``, ``size``, ``location``, + ``ImageId``, ``InstanceType``, ``KeyName``. + +.. salt:event:: salt/cloud//requesting/failed + + Fired when the create request is sent but the provider returns an error + before an instance ID is allocated. Currently emitted by the EC2 driver + when a spot instance request fails. + + :var error: the error message returned by the provider. + +.. salt:event:: salt/cloud//waiting_for_spot + + Fired by the EC2 driver after a spot-instance request has been submitted + and ``salt-cloud`` is waiting for the spot request to be fulfilled. + +.. salt:event:: salt/cloud//querying + + Fired when ``salt-cloud`` begins polling the cloud provider for the + instance's network address(es) after a successful create request. + + :var instance_id: the provider-assigned ID of the new instance. + +.. salt:event:: salt/cloud//tagging + + Fired when ``salt-cloud`` applies tags to a newly-created instance. + Emitted by drivers that tag instances at create time (e.g. EC2, packet). + + :var tags: dictionary of tags being applied. + +.. salt:event:: salt/cloud//waiting_for_ssh + + Fired once the instance has an IP address and ``salt-cloud`` begins + waiting for SSH to become available so that the deploy script can be + uploaded. + + :var ip_address: the IP address ``salt-cloud`` will connect to. + +.. salt:event:: salt/cloud//deploying + + Fired immediately before the deploy script (``bootstrap-salt.sh`` by + default) or the Windows installer is uploaded and executed on the + instance. + + :var kwargs: the deploy keyword arguments. Sensitive keys + (``password``, ``private_key``, ``minion_pem``, ``minion_pub``, + etc.) are stripped before the event is fired. + +.. salt:event:: salt/cloud//deploy_script + + Fired once the Linux/Unix deploy script has finished executing on the + new instance. + +.. salt:event:: salt/cloud//deploy_windows + + Fired once the Windows installer has finished executing on the new + instance. + +.. salt:event:: salt/cloud//created + + Fired once the instance has been fully created and (when applicable) + Salted. This is the final tag fired by the create path. + + :var name: name of the VM that was created. + :var profile: the cloud profile used. + :var provider: the cloud provider used. + :var instance_id: the provider-assigned ID of the instance. + +.. salt:event:: salt/cloud//destroying + + Fired when ``salt-cloud`` requests destruction of an instance. + + :var name: name of the VM being destroyed. + :var instance_id: the provider-assigned ID of the instance. + +.. salt:event:: salt/cloud//destroyed + + Fired once the instance has been destroyed. + + :var name: name of the VM that was destroyed. + :var instance_id: the provider-assigned ID of the instance. + +Power-State Events +================== + +These tags are fired by drivers that support pausing, starting, stopping, +rebooting, or resizing existing instances through ``salt-cloud -a``. + +.. salt:event:: salt/cloud//starting + + Fired when ``salt-cloud`` requests that a stopped instance be started. + +.. salt:event:: salt/cloud//started + + Fired once the instance has been started. + +.. salt:event:: salt/cloud//stopping + + Fired when ``salt-cloud`` requests that a running instance be stopped. + +.. salt:event:: salt/cloud//stopped + + Fired once the instance has been stopped. + +.. salt:event:: salt/cloud//rebooting + + Fired when ``salt-cloud`` requests that an instance be rebooted. + +.. salt:event:: salt/cloud//rebooted + + Fired once the reboot has been requested. This event marks the request + completing; the instance may still be in the process of restarting. + +.. salt:event:: salt/cloud//resizing + + Fired when ``salt-cloud`` requests that an instance be resized. + +.. salt:event:: salt/cloud//resized + + Fired once the resize has completed. + +.. salt:event:: salt/cloud//deleting + + Fired by drivers that distinguish a "delete" call from a "destroy" + (e.g. Hetzner) when ``salt-cloud`` requests deletion of a resource. + +.. salt:event:: salt/cloud//deleted + + Fired by drivers that distinguish a "delete" call from a "destroy" + once the delete operation has completed. + +Reactor Hook Events +=================== + +These tags are emitted by the cloud cache subsystem when running +``salt-cloud --full-query`` (or the ``cloud.full_query`` runner) and are +intended for use with the ``salt-cloud-reactor`` formula. See the +`salt-cloud-reactor`_ formula for example reactors. + +.. _salt-cloud-reactor: https://github.com/saltstack-formulas/salt-cloud-reactor + +.. salt:event:: salt/cloud//query_reactor + + Fired by ``salt-cloud --full-query`` when a node is observed and the + reactor cache is enabled. + +.. salt:event:: salt/cloud//ssh_ready_reactor + + Fired when the cloud cache subsystem detects that SSH on an instance is + ready to receive connections. + +.. salt:event:: salt/cloud//cache_node_new + + Fired the first time a node is observed during a full-query refresh. + +.. salt:event:: salt/cloud//cache_node_missing + + Fired when a node that was previously cached is no longer reported by + the cloud provider during a full-query refresh. + +.. salt:event:: salt/cloud//cache_node_diff + + Fired when a node's cached data has changed since the previous + full-query refresh. + +Resource Events +=============== + +Several drivers expose function calls that create, delete, attach, or detach +non-VM resources such as block volumes, load balancers, snapshots, public +IPs, networks, and firewalls. The general pattern is +``salt/cloud//`` where ```` is the resource type +or the resource's name, not a VM name. + +Volume / disk events +-------------------- + +.. salt:event:: salt/cloud//attaching_volumes + + Fired by the EC2 driver when one or more EBS volumes are being attached + during VM creation. + +.. salt:event:: salt/cloud/disk/creating +.. salt:event:: salt/cloud/disk/created +.. salt:event:: salt/cloud/disk/deleting +.. salt:event:: salt/cloud/disk/deleted +.. salt:event:: salt/cloud/disk/attaching +.. salt:event:: salt/cloud/disk/attached +.. salt:event:: salt/cloud/disk/detaching +.. salt:event:: salt/cloud/disk/detached + + Fired by the GCE driver around persistent-disk operations. + +.. salt:event:: salt/cloud//destroying +.. salt:event:: salt/cloud//destroyed +.. salt:event:: salt/cloud//detaching +.. salt:event:: salt/cloud//detached + + Fired by the OpenStack driver around block-volume destroy and detach + operations. + +.. salt:event:: salt/cloud/block_volume_/tagging + + Fired by the EC2 driver when block-volume tags are applied as part of + instance creation. + +Snapshot events +--------------- + +.. salt:event:: salt/cloud/snapshot/creating +.. salt:event:: salt/cloud/snapshot/created +.. salt:event:: salt/cloud/snapshot/deleting +.. salt:event:: salt/cloud/snapshot/deleted + + Fired by the GCE driver around snapshot operations. + +Network events +-------------- + +.. salt:event:: salt/cloud/net/creating +.. salt:event:: salt/cloud/net/created +.. salt:event:: salt/cloud/net/deleting +.. salt:event:: salt/cloud/net/deleted +.. salt:event:: salt/cloud/subnet/creating +.. salt:event:: salt/cloud/subnet/created +.. salt:event:: salt/cloud/subnet/deleting +.. salt:event:: salt/cloud/subnet/deleted + + Fired by the GCE driver around VPC network and subnet operations. + +.. salt:event:: salt/cloud/address/creating +.. salt:event:: salt/cloud/address/created +.. salt:event:: salt/cloud/address/deleting +.. salt:event:: salt/cloud/address/deleted + + Fired by the GCE driver around static external-IP operations. + +.. salt:event:: salt/cloud/firewall/creating +.. salt:event:: salt/cloud/firewall/created +.. salt:event:: salt/cloud/firewall/deleting +.. salt:event:: salt/cloud/firewall/deleted + + Fired by the GCE driver around firewall-rule operations. + +Load balancer / health check events +----------------------------------- + +.. salt:event:: salt/cloud/loadbalancer/creating +.. salt:event:: salt/cloud/loadbalancer/created +.. salt:event:: salt/cloud/loadbalancer/deleting +.. salt:event:: salt/cloud/loadbalancer/deleted +.. salt:event:: salt/cloud/loadbalancer/attaching +.. salt:event:: salt/cloud/loadbalancer/attached +.. salt:event:: salt/cloud/loadbalancer/detaching +.. salt:event:: salt/cloud/loadbalancer/detached + + Fired by the GCE driver around load-balancer pool operations and + backend attachment. + +.. salt:event:: salt/cloud/healthcheck/creating +.. salt:event:: salt/cloud/healthcheck/created +.. salt:event:: salt/cloud/healthcheck/deleting +.. salt:event:: salt/cloud/healthcheck/deleted + + Fired by the GCE driver around HTTP health-check operations. + +Spot-instance events +-------------------- + +.. salt:event:: salt/cloud/spot_request_/tagging + + Fired by the EC2 driver when a spot-instance request is tagged. + +Filtering Event Payloads +======================== + +The keys included in each event payload can be filtered using the +``filter_events`` block in the master configuration. See +:ref:`salt-cloud-with-reactor` for examples and the list of tags supported +by the filter mechanism. diff --git a/doc/topics/cloud/index.rst b/doc/topics/cloud/index.rst index ce0bd82232a7..ad33f8eae904 100644 --- a/doc/topics/cloud/index.rst +++ b/doc/topics/cloud/index.rst @@ -173,6 +173,13 @@ Feature Comparison Features +Reference +========= +.. toctree:: + :maxdepth: 3 + + Salt Cloud Events Reference + Tutorials ========= .. toctree:: diff --git a/doc/topics/cloud/reactor.rst b/doc/topics/cloud/reactor.rst index ad81bad49cfb..c3cabc5e5091 100644 --- a/doc/topics/cloud/reactor.rst +++ b/doc/topics/cloud/reactor.rst @@ -1,3 +1,5 @@ +.. _salt-cloud-with-reactor: + ======================================= Using Salt Cloud with the Event Reactor ======================================= @@ -38,9 +40,12 @@ Available Events ================ When an instance is created in Salt Cloud, whether by map, profile, or directly -through an API, a minimum of five events are normally fired. More may be -available, depending upon the cloud provider being used. Some of the common -events are described below. +through an API, several events are normally fired. The exact set depends on +the cloud driver. For a complete reference of every tag emitted by Salt Cloud +(create, destroy, action, and per-driver resource events), see +:ref:`cloud-events-reference`. + +The most commonly used events fired during VM creation are described below. salt/cloud//creating ------------------------------- @@ -115,6 +120,14 @@ instance information to the user and exiting. The payload for this event contains little more than the initial ``creating`` event. This event is required in all cloud providers. +salt/cloud//destroying and destroyed +----------------------------------------------- + +When an instance is destroyed using the ``-d`` option, Salt Cloud first fires +a ``destroying`` event, then asks the cloud provider to terminate the instance, +and finally fires a ``destroyed`` event. Both events normally include the +``name`` and ``instance_id`` keys in their payloads. + Filtering Events ================ diff --git a/doc/topics/event/master_events.rst b/doc/topics/event/master_events.rst index c6ee9d076604..0c9ca6095923 100644 --- a/doc/topics/event/master_events.rst +++ b/doc/topics/event/master_events.rst @@ -179,6 +179,10 @@ Salt Minion. Instead, ``salt-cloud`` events are fired on behalf of a VM. This is because the minion-to-be may not yet exist to fire events to or also may have been destroyed. +The tags listed below are the most commonly used lifecycle events. The full +catalog of tags emitted by Salt Cloud (including power-state, resource, and +reactor-hook events) is documented in :ref:`cloud-events-reference`. + This behavior is reflected by the ``name`` variable in the event data for ``salt-cloud`` events as compared to the ``id`` variable for Salt Minion-triggered events. diff --git a/tests/pytests/unit/cloud/test_cloud_events_documented.py b/tests/pytests/unit/cloud/test_cloud_events_documented.py new file mode 100644 index 000000000000..e0a294528b3e --- /dev/null +++ b/tests/pytests/unit/cloud/test_cloud_events_documented.py @@ -0,0 +1,188 @@ +""" +Capture-and-assert tests that ensure every event tag emitted by Salt Cloud is +documented on the :ref:`cloud-events-reference` page. + +The tests scan ``salt/cloud/clouds/`` together with ``salt/cloud/__init__.py`` +and ``salt/utils/cloud.py`` for literal ``"salt/cloud/..."`` strings used as +the ``tag=`` argument to ``cloud.fire_event``, normalise each tag into its +```` (and, where relevant, ````) segments, and verify that +the normalised tag is referenced in ``doc/topics/cloud/events.rst``. + +If a driver introduces a new event tag the documentation page must be updated +to mention it; otherwise this test fails. +""" + +import pathlib +import re + +import pytest + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[4] +EVENTS_DOC = REPO_ROOT / "doc" / "topics" / "cloud" / "events.rst" + +SCAN_PATHS = [ + REPO_ROOT / "salt" / "cloud" / "clouds", + REPO_ROOT / "salt" / "cloud" / "__init__.py", + REPO_ROOT / "salt" / "utils" / "cloud.py", +] + +TAG_RE = re.compile(r"""['"](salt/cloud/[^'"]+)['"]""") + + +def _normalise(tag): + """ + Replace per-VM / per-resource interpolation placeholders with the + ```` / ```` / ```` tokens used in the docs. + """ + parts = tag.split("/") + # parts[0:2] == ['salt', 'cloud'] + # parts[2] == resource (vm name or fixed resource type) + # parts[3:] == task path + if len(parts) < 4: + return tag + + resource = parts[2] + task = "/".join(parts[3:]) + + # Strip the ``cache_node_*`` family back to the documented form. The + # resource segment is the node name there. + if task.startswith("cache_node_"): + return f"salt/cloud//{task}" + + # Fixed resource buckets used by the GCE driver. + if resource in { + "disk", + "snapshot", + "net", + "subnet", + "address", + "firewall", + "loadbalancer", + "healthcheck", + }: + return f"salt/cloud/{resource}/{task}" + + # Block-volume / spot-request templated buckets. + if resource.startswith("spot_request_"): + return f"salt/cloud/spot_request_/{task}" + if resource.startswith("block_volume_"): + return f"salt/cloud/block_volume_/{task}" + + # Volume-name tags emitted by the OpenStack driver use ``{volume.name}``. + if "volume" in resource: + return f"salt/cloud//{task}" + + # Any remaining placeholder is a per-VM runtime value. + if "{" in resource: + return f"salt/cloud//{task}" + + return f"salt/cloud/{resource}/{task}" + + +def _gather_tags(): + tags = set() + for path in SCAN_PATHS: + if path.is_dir(): + files = sorted(path.glob("*.py")) + else: + files = [path] + for source in files: + text = source.read_text(encoding="utf-8") + for match in TAG_RE.findall(text): + tags.add(match) + return tags + + +def _doc_text(): + return EVENTS_DOC.read_text(encoding="utf-8") + + +@pytest.fixture(scope="module") +def emitted_tags(): + return {_normalise(tag) for tag in _gather_tags()} + + +@pytest.fixture(scope="module") +def documented_tags_text(): + return _doc_text() + + +def test_events_reference_page_exists(): + assert EVENTS_DOC.is_file(), ( + "doc/topics/cloud/events.rst is required by Salt Cloud event " + "documentation tests" + ) + + +def test_documented_tags_render_under_salt_event_directive(documented_tags_text): + """ + Every event documented on the reference page must be declared with the + ``.. salt:event::`` directive so the rendered HTML includes a permalink. + """ + directive_count = documented_tags_text.count(".. salt:event:: salt/cloud/") + # The reference page is expected to declare at least the documented + # lifecycle, power-state, reactor-hook and resource events. The number is + # intentionally generous so that adding new tags does not require touching + # this assertion. + assert directive_count >= 30 + + +@pytest.mark.parametrize( + "tag", + [ + "salt/cloud//creating", + "salt/cloud//requesting", + "salt/cloud//querying", + "salt/cloud//waiting_for_ssh", + "salt/cloud//deploying", + "salt/cloud//deploy_script", + "salt/cloud//deploy_windows", + "salt/cloud//created", + "salt/cloud//destroying", + "salt/cloud//destroyed", + "salt/cloud//tagging", + "salt/cloud//requesting/failed", + "salt/cloud//starting", + "salt/cloud//started", + "salt/cloud//stopping", + "salt/cloud//stopped", + "salt/cloud//rebooting", + "salt/cloud//rebooted", + "salt/cloud//resizing", + "salt/cloud//resized", + "salt/cloud//query_reactor", + "salt/cloud//ssh_ready_reactor", + "salt/cloud//cache_node_new", + "salt/cloud//cache_node_missing", + "salt/cloud//cache_node_diff", + "salt/cloud/disk/created", + "salt/cloud/snapshot/created", + "salt/cloud/net/created", + "salt/cloud/subnet/created", + "salt/cloud/address/created", + "salt/cloud/firewall/created", + "salt/cloud/loadbalancer/created", + "salt/cloud/healthcheck/created", + "salt/cloud/spot_request_/tagging", + "salt/cloud/block_volume_/tagging", + ], +) +def test_documented_tag_appears_in_reference(tag, documented_tags_text): + assert tag in documented_tags_text, ( + f"{tag} is in the canonical Salt Cloud event tag list but is missing " + "from doc/topics/cloud/events.rst" + ) + + +def test_every_emitted_tag_is_documented(emitted_tags, documented_tags_text): + """ + The set of tags emitted by the cloud drivers must be a subset of the + documented set. If a driver introduces a new tag, the reference page must + be updated to mention it. + """ + undocumented = sorted(t for t in emitted_tags if t not in documented_tags_text) + assert ( + not undocumented + ), "Undocumented salt-cloud event tags emitted from the code: " + ", ".join( + undocumented + ) From 4a784e6c2e7d23cf87605a133bd21c5973d1fce7 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 25 Jun 2026 16:29:49 -0700 Subject: [PATCH 2/7] Drop required script() language and stale Linode libcloud example --- changelog/65323.fixed.md | 1 + doc/topics/cloud/cloud.rst | 29 ++++++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 changelog/65323.fixed.md diff --git a/changelog/65323.fixed.md b/changelog/65323.fixed.md new file mode 100644 index 000000000000..1837061373aa --- /dev/null +++ b/changelog/65323.fixed.md @@ -0,0 +1 @@ +Updated the *Writing Cloud Driver Modules* guide to stop describing ``script()`` as required for new drivers; ``salt.utils.cloud.bootstrap()`` already resolves the deploy script via ``os_script()``. The stale "use Linode as a libcloud example" reference has also been replaced with CloudStack and Dimension Data, and a note explains that Linode now uses the REST API directly. diff --git a/doc/topics/cloud/cloud.rst b/doc/topics/cloud/cloud.rst index bfb7aff6bdc5..f733ee3d6311 100644 --- a/doc/topics/cloud/cloud.rst +++ b/doc/topics/cloud/cloud.rst @@ -54,10 +54,18 @@ The most important function that does need to be manually written is the created by the cloud host, wait for it to become available, and then (optionally) log in and install Salt on it. -A good example to follow for writing a cloud driver module based on libcloud -is the module provided for Linode: +Good examples to follow for writing a cloud driver module based on libcloud +are the modules provided for CloudStack and Dimension Data: -https://github.com/saltstack/salt/tree/|repo_primary_branch|/salt/cloud/clouds/linode.py +https://github.com/saltstack/salt/tree/|repo_primary_branch|/salt/cloud/clouds/cloudstack.py + +https://github.com/saltstack/salt/tree/|repo_primary_branch|/salt/cloud/clouds/dimensiondata.py + +.. note:: + + Salt's Linode driver no longer uses libcloud. It was rewritten to use the + Linode REST API directly; see ``salt/cloud/clouds/linode.py`` for an + example of a non-libcloud driver instead. The basic flow of a ``create()`` function is as follows: @@ -238,10 +246,17 @@ normally called using the ``--list-sizes`` option. The script() Function --------------------- -This function builds the deploy script to be used on the remote machine. It is -likely to be moved into the ``salt.utils.cloud`` library in the near future, as -it is very generic and can usually be copied wholesale from another module. An -excellent example is in the Azure driver. +This function used to be required by every cloud driver. It is no longer +required: the deploy-script lookup is now handled centrally by +``salt.utils.cloud.bootstrap()``, which reads the ``script`` value from the +profile or provider configuration and resolves it via +``salt.utils.cloud.os_script()``. New drivers do not need to define their +own ``script()`` function. + +Existing drivers continue to ship a ``script()`` wrapper for backward +compatibility. It is safe to copy this wrapper unchanged from another driver +(for example ``salt/cloud/clouds/digitalocean.py``) when porting an older +driver, but it should not be required for any new code paths. The destroy() Function ---------------------- From b896faed79acf87b5aa6f7cc29305f474f601a98 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 25 Jun 2026 16:29:54 -0700 Subject: [PATCH 3/7] Remove misleading OpenNebula create() kwarg CLI example --- changelog/64520.fixed.md | 1 + salt/cloud/clouds/opennebula.py | 34 ++++++++------- .../cloud/clouds/test_opennebula_options.py | 42 +++++++++++++++++++ 3 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 changelog/64520.fixed.md create mode 100644 tests/pytests/unit/cloud/clouds/test_opennebula_options.py diff --git a/changelog/64520.fixed.md b/changelog/64520.fixed.md new file mode 100644 index 000000000000..3fcd9e5d59d9 --- /dev/null +++ b/changelog/64520.fixed.md @@ -0,0 +1 @@ +Removed the misleading ``salt-cloud -p my-opennebula-profile vm_name memory=16384 cpu=2.5 vcpu=16`` example from the OpenNebula ``create()`` docstring. ``salt-cloud -p`` treats trailing positional arguments as additional VM names; the rewritten docstring explains the workaround (a dedicated profile or a cloud map) for per-VM overrides. diff --git a/salt/cloud/clouds/opennebula.py b/salt/cloud/clouds/opennebula.py index 7fa1428f834d..c201a4567e07 100644 --- a/salt/cloud/clouds/opennebula.py +++ b/salt/cloud/clouds/opennebula.py @@ -928,29 +928,35 @@ def create(vm_): Create a single VM from a data dict. vm\_ - The dictionary use to create a VM. + The dictionary used to create a VM. The following keys are read from + the active profile (and may also be overridden in the cloud map): - Optional vm\_ dict options for overwriting template: + region_id + Optional - OpenNebula Zone ID. - region_id - Optional - OpenNebula Zone ID + memory + Optional - In MB. - memory - Optional - In MB + cpu + Optional - Percent of host CPU to allocate. - cpu - Optional - Percent of host CPU to allocate + vcpu + Optional - Number of vCPUs to allocate. - vcpu - Optional - Amount of vCPUs to allocate + .. note:: - CLI Example: + ``salt-cloud -p`` accepts only a profile name and a list of VM names + on the command line. To override ``memory``, ``cpu``, ``vcpu``, or + any other profile setting for a single VM, define a separate profile + (or use a cloud map) rather than passing key=value arguments to + ``-p``; trailing positional arguments are interpreted as additional + VM names and will create extra VMs. - .. code-block:: bash + CLI Example: - salt-cloud -p my-opennebula-profile vm_name + .. code-block:: bash - salt-cloud -p my-opennebula-profile vm_name memory=16384 cpu=2.5 vcpu=16 + salt-cloud -p my-opennebula-profile vm_name """ try: diff --git a/tests/pytests/unit/cloud/clouds/test_opennebula_options.py b/tests/pytests/unit/cloud/clouds/test_opennebula_options.py new file mode 100644 index 000000000000..1cf0883f5e30 --- /dev/null +++ b/tests/pytests/unit/cloud/clouds/test_opennebula_options.py @@ -0,0 +1,42 @@ +""" +Documentation-tracking tests for the OpenNebula driver. + +Backed by issue #64520, the docstring on ``opennebula.create`` previously +documented a ``salt-cloud -p`` invocation with trailing ``key=value`` pairs. +``salt-cloud -p`` does not parse those into per-VM overrides; it creates one +VM per positional argument. The note in the docstring must remain so the +generated docs are not misleading again. +""" + +from salt.cloud.clouds import opennebula + + +def test_create_docstring_drops_kwarg_cli_example(): + """ + The misleading ``salt-cloud -p ... memory=... cpu=... vcpu=...`` example + must not reappear in the create() docstring. + """ + doc = opennebula.create.__doc__ or "" + assert "memory=16384 cpu=2.5 vcpu=16" not in doc + + +def test_create_docstring_documents_override_workaround(): + """ + The replacement docstring must explain that profile-overriding kwargs + cannot be passed to ``salt-cloud -p`` and recommend a separate profile + or cloud map instead. + """ + doc = opennebula.create.__doc__ or "" + assert "trailing positional arguments are interpreted as additional" in doc + assert "salt-cloud -p my-opennebula-profile vm_name" in doc + + +def test_create_docstring_documents_supported_overrides(): + """ + The supported ``vm_`` keys (``region_id``, ``memory``, ``cpu``, + ``vcpu``) must remain documented so the driver reference page lists + them. + """ + doc = opennebula.create.__doc__ or "" + for option in ("region_id", "memory", "cpu", "vcpu"): + assert f"{option}\n" in doc, f"{option} no longer documented on create()" From 5f4f3caafa205ce6363e20b808f7a1eab6f5ff14 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 25 Jun 2026 16:30:11 -0700 Subject: [PATCH 4/7] Document every register_image kwarg in the EC2 driver --- changelog/60679.fixed.md | 1 + salt/cloud/clouds/ec2.py | 56 +++++++++++++++++-- .../unit/cloud/clouds/test_ec2_options.py | 45 +++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 changelog/60679.fixed.md create mode 100644 tests/pytests/unit/cloud/clouds/test_ec2_options.py diff --git a/changelog/60679.fixed.md b/changelog/60679.fixed.md new file mode 100644 index 000000000000..0139854d3abf --- /dev/null +++ b/changelog/60679.fixed.md @@ -0,0 +1 @@ +Documented every keyword argument accepted by the EC2 cloud ``register_image`` function (``ami_name``, ``description``, ``architecture``, ``virtualization_type``, ``root_device_name``, ``snapshot_id``, ``volume_type``, ``block_device_mapping``), including a multi-volume ``block_device_mapping`` CLI example that survives the shell parser. diff --git a/salt/cloud/clouds/ec2.py b/salt/cloud/clouds/ec2.py index 47eb71af3002..fa1ea56afe5c 100644 --- a/salt/cloud/clouds/ec2.py +++ b/salt/cloud/clouds/ec2.py @@ -4072,14 +4072,62 @@ def _toggle_delvol( def register_image(kwargs=None, call=None): """ - Create an ami from a snapshot + Create an AMI from one or more EBS snapshots. - CLI Example: + Accepted keyword arguments: + + ami_name : required + Name of the AMI to register. + + description + Free-form description for the AMI. + + architecture + AMI architecture (for example ``x86_64`` or ``arm64``). + + virtualization_type + Virtualization type to register the AMI under (``hvm`` or + ``paravirtual``). + + root_device_name + Device name to mark as the AMI's root device (for example + ``/dev/xvda``). Required when ``block_device_mapping`` is **not** + supplied: in that case Salt Cloud builds a single-device mapping + from ``root_device_name``, ``snapshot_id`` and ``volume_type``. + + snapshot_id + Snapshot ID to use for the root volume. Required when + ``block_device_mapping`` is not supplied. + + volume_type + EBS volume type to use for the auto-generated root mapping. Default + is ``gp2``. Ignored when ``block_device_mapping`` is supplied. + + block_device_mapping + Explicit list of block device mappings. Each entry is a dict in + the AWS ``RegisterImage`` format. Use this when registering an AMI + with more than one volume, or with non-default mapping options. + + CLI Examples: .. code-block:: bash - salt-cloud -f register_image my-ec2-config ami_name=my_ami description="my description" - root_device_name=/dev/xvda snapshot_id=snap-xxxxxxxx + # Register an AMI from a single snapshot. Salt Cloud generates + # the block-device mapping from the snapshot_id / root_device_name + # arguments. + salt-cloud -f register_image my-ec2-config \\ + ami_name=my_ami description="my description" \\ + architecture=x86_64 root_device_name=/dev/xvda \\ + snapshot_id=snap-xxxxxxxx + + # Register an AMI with an explicit multi-device mapping. The + # block_device_mapping value must be passed as YAML (or JSON) on + # the command line because the CLI shell will not parse a bare + # list-of-dicts literal: + salt-cloud -f register_image my-ec2-config \\ + ami_name=multi_vol_ami architecture=x86_64 \\ + root_device_name=/dev/xvda \\ + block_device_mapping='[{"DeviceName": "/dev/xvda", "Ebs": {"SnapshotId": "snap-1111", "VolumeType": "gp2"}}, {"DeviceName": "/dev/sdb", "Ebs": {"SnapshotId": "snap-2222", "VolumeType": "gp2"}}]' """ if call != "function": diff --git a/tests/pytests/unit/cloud/clouds/test_ec2_options.py b/tests/pytests/unit/cloud/clouds/test_ec2_options.py new file mode 100644 index 000000000000..256e9c66b6f8 --- /dev/null +++ b/tests/pytests/unit/cloud/clouds/test_ec2_options.py @@ -0,0 +1,45 @@ +""" +Documentation-tracking tests for the EC2 driver. + +Backed by issue #60679: the ``register_image`` function previously documented +only a single-snapshot CLI example and did not list the kwargs accepted by +the function. The replacement docstring must list every kwarg consulted by +the function body so the generated reference page is complete. +""" + +from salt.cloud.clouds import ec2 + +REGISTER_IMAGE_DOCUMENTED_KWARGS = ( + "ami_name", + "description", + "architecture", + "virtualization_type", + "root_device_name", + "snapshot_id", + "volume_type", + "block_device_mapping", +) + + +def test_register_image_documents_every_kwarg(): + doc = ec2.register_image.__doc__ or "" + for option in REGISTER_IMAGE_DOCUMENTED_KWARGS: + assert option in doc, ( + f"register_image() kwarg {option!r} consulted by the function " + "but absent from the docstring" + ) + + +def test_register_image_documents_multi_volume_cli_example(): + doc = ec2.register_image.__doc__ or "" + # The shell parsing caveat described in #60679 must be documented so + # users know to pass block_device_mapping as JSON/YAML. + assert "block_device_mapping=" in doc + assert "DeviceName" in doc and "SnapshotId" in doc + assert "/dev/xvda" in doc and "/dev/sdb" in doc + + +def test_register_image_documents_single_snapshot_example(): + doc = ec2.register_image.__doc__ or "" + assert "snapshot_id=snap-xxxxxxxx" in doc + assert "ami_name=my_ami" in doc From e3bb75a9d742cdf8aa0e399343cd6e94b26b925f Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 25 Jun 2026 16:30:27 -0700 Subject: [PATCH 5/7] Document VMware customization_spec and Ubuntu hostname issue --- changelog/57933.fixed.md | 1 + doc/topics/cloud/vmware.rst | 36 ++++++++++++++++ .../unit/cloud/clouds/test_vmware_options.py | 41 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 changelog/57933.fixed.md create mode 100644 tests/pytests/unit/cloud/clouds/test_vmware_options.py diff --git a/changelog/57933.fixed.md b/changelog/57933.fixed.md new file mode 100644 index 000000000000..ffa743fbf890 --- /dev/null +++ b/changelog/57933.fixed.md @@ -0,0 +1 @@ +Documented the VMware cloud driver's ``customization_spec`` profile option (a name of a vCenter Customization Specification to apply at clone time) and added a Known Issues section that points at the ongoing Ubuntu hostname-not-set bug (issue #55889) and its workaround. diff --git a/doc/topics/cloud/vmware.rst b/doc/topics/cloud/vmware.rst index e58b9b7ff909..891bf15487ae 100644 --- a/doc/topics/cloud/vmware.rst +++ b/doc/topics/cloud/vmware.rst @@ -514,6 +514,25 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or ``customization: False`` is set, the new virtual machine will not be customized. Default is ``customization: True``. +``customization_spec`` + Name of an existing vCenter Customization Specification to apply when + cloning. When set, Salt Cloud looks up the spec by name on the target + vCenter and uses it for the clone's guest customization instead of + building one from the profile's ``devices.network`` block. The + ``customization`` option must remain ``True`` (the default) for the spec + to be applied. Example: + + .. code-block:: yaml + + customization_spec: my-linux-spec + + Use this option when you have already defined a reusable customization + specification (DNS suffixes, time zone, license keys, sysprep details, + etc.) in vCenter that should be applied to VMs created through Salt + Cloud. If both ``customization_spec`` and an inline ``devices.network`` + customization are needed, define them on the customization spec in + vCenter; only one customization source is used per clone. + ``private_key`` Specify the path to the private key to use to be able to ssh to the VM. @@ -822,3 +841,20 @@ can be set in the cloud profile as shown in example below: size: 42 Hard disk 2: mode: 'independent_nonpersistent' + + +Known Issues +============ + +Ubuntu template hostname is not set on clone +-------------------------------------------- + +When cloning from an Ubuntu template, the guest may boot with the template's +hostname (often ``ubuntu``) instead of the value derived from the profile +name. This is a bug in the VMware driver's guest customization path for +Linux guests that use ``cloud-init``; see +`issue #55889 `_ for the +current status. As a workaround, supply a ``customization_spec`` (see the +profile-level option above) that sets ``computerName`` to the desired value, +or apply a state after creation that writes ``/etc/hostname`` and runs +``hostnamectl set-hostname``. diff --git a/tests/pytests/unit/cloud/clouds/test_vmware_options.py b/tests/pytests/unit/cloud/clouds/test_vmware_options.py new file mode 100644 index 000000000000..e982c6cbb18f --- /dev/null +++ b/tests/pytests/unit/cloud/clouds/test_vmware_options.py @@ -0,0 +1,41 @@ +""" +Documentation-tracking tests for the VMware driver. + +Backed by issue #57933 (customization_spec missing from docs) and #55889 +(Ubuntu hostname not set). +""" + +import pathlib + +from salt.cloud.clouds import vmware + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[5] +VMWARE_DOC = REPO_ROOT / "doc" / "topics" / "cloud" / "vmware.rst" + + +def _doc_text(): + return VMWARE_DOC.read_text(encoding="utf-8") + + +def test_customization_spec_is_documented(): + doc = _doc_text() + assert "``customization_spec``" in doc + # The doc must explain the dependency on customization=True, otherwise + # users will be surprised when their spec is silently ignored. + assert "``customization``" in doc + + +def test_customization_spec_used_by_create(): + """ + The driver actually reads ``customization_spec`` from the profile; this + test guarantees the option name survives any future refactor of + create(). + """ + source = pathlib.Path(vmware.__file__).read_text(encoding="utf-8") + assert '"customization_spec"' in source + assert "get_customizationspec_ref" in source + + +def test_ubuntu_hostname_known_issue_referenced(): + doc = _doc_text() + assert "issue #55889" in doc From 226d4c744c88999b1f6d3f5bac4f952ddbb7fafc Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 25 Jun 2026 16:30:37 -0700 Subject: [PATCH 6/7] Document AzureARM bootstrap_interface and known make_master/query bugs --- changelog/65063.fixed.md | 1 + doc/topics/cloud/azurearm.rst | 51 +++++++++++++++++++ .../cloud/clouds/test_azurearm_options.py | 48 +++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 changelog/65063.fixed.md create mode 100644 tests/pytests/unit/cloud/clouds/test_azurearm_options.py diff --git a/changelog/65063.fixed.md b/changelog/65063.fixed.md new file mode 100644 index 000000000000..b111346be4bd --- /dev/null +++ b/changelog/65063.fixed.md @@ -0,0 +1 @@ +Documented the Azure ARM cloud driver's ``bootstrap_interface`` profile option (``public`` or ``private``) and clarified that the generic ``ssh_interface`` / ``win_interface`` options used by other drivers are **not** consulted by ``azurearm``. Added notes referencing the make-master traceback (issue #58096) and the ``list index out of range`` query error (issue #65064) so users can avoid the misconfiguration that triggers them. diff --git a/doc/topics/cloud/azurearm.rst b/doc/topics/cloud/azurearm.rst index cc9b21228199..bddf6ff35066 100644 --- a/doc/topics/cloud/azurearm.rst +++ b/doc/topics/cloud/azurearm.rst @@ -266,6 +266,57 @@ allocate_public_ip Optional. Default is ``False``. If set to ``True``, a public IP will be created and assigned to the VM. +bootstrap_interface +------------------- +Optional. Default is ``public``. Selects which network address the Azure ARM +driver waits on (and uses for the Salt deploy connection) once the VM has +been created. Valid values are: + +``public`` + Use the first address from the VM's ``public_ips``. This is the default + and matches the historical behaviour. Requires ``allocate_public_ip: + True``. + +``private`` + Use the first address from the VM's ``private_ips``. Use this value when + the Salt master can reach the VM over the VNet (for example a + ``make_master`` deployment or a private build agent) and you do not want + to allocate a public IP. + +.. note:: + + ``bootstrap_interface`` is specific to the Azure ARM driver. The + generic ``ssh_interface`` option (documented under other drivers such + as AWS, GCE and ProfitBricks) is **not** consulted by ``azurearm``; + use ``bootstrap_interface`` instead. ``win_interface`` is also not + consulted: Windows deploys use the same address selected by + ``bootstrap_interface``. + +.. note:: + + Creating a salt-master VM with ``make_master: True`` against Azure ARM + is currently affected by an upstream bug + (`issue #58096 `_) that + raises ``ValueError: dictionary update sequence element #0 has length + 1; 2 is required`` from ``salt.utils.cloud.master_config``. As a + workaround, install the salt-master with a state run against a + standalone VM (``make_master: False``) and accept the new minion's key + on that master, instead of bootstrapping the master through the cloud + map. + +.. note:: + + Profiles that request a Windows image with + ``allocate_public_ip: False`` and the default + ``bootstrap_interface: public`` can fail with + ``There was a query error: list index out of range`` once the VM has + been requested (see + `issue #65064 `_). + The error originates from ``_query_node_data`` reading + ``data["public_ips"][0]`` against an instance that was not allocated + one. Set ``bootstrap_interface: private`` whenever + ``allocate_public_ip`` is ``False``. + load_balancer ------------- Optional. The load-balancer for the VM's network interface to join. If diff --git a/tests/pytests/unit/cloud/clouds/test_azurearm_options.py b/tests/pytests/unit/cloud/clouds/test_azurearm_options.py new file mode 100644 index 000000000000..ef32fab0bef7 --- /dev/null +++ b/tests/pytests/unit/cloud/clouds/test_azurearm_options.py @@ -0,0 +1,48 @@ +""" +Documentation-tracking tests for the AzureARM driver. + +Backed by issues #65063 (inconsistent ssh_interface/bootstrap_interface +documentation) and #58096 (make_master traceback) / #65064 (query error on +Windows with no public IP). These assertions hold the docs page accountable +for the runtime behaviour described in the ``create()`` body. +""" + +import pathlib + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[5] +AZUREARM_DOC = REPO_ROOT / "doc" / "topics" / "cloud" / "azurearm.rst" + + +def _doc_text(): + return AZUREARM_DOC.read_text(encoding="utf-8") + + +def test_azurearm_doc_documents_bootstrap_interface(): + doc = _doc_text() + assert "bootstrap_interface" in doc + # The two valid values must both appear under the option's section so + # users do not have to read the source to discover them. + assert "``public``" in doc and "``private``" in doc + + +def test_azurearm_doc_calls_out_ssh_interface_is_not_used(): + """ + ``ssh_interface`` is honoured by other drivers; AzureARM uses + ``bootstrap_interface``. The note about that must be present. + """ + doc = _doc_text() + assert ( + "generic ``ssh_interface`` option" in doc + and "is **not** consulted by ``azurearm``" in doc + ) + + +def test_azurearm_doc_references_make_master_bug(): + doc = _doc_text() + assert "issue #58096" in doc + + +def test_azurearm_doc_references_query_error_bug(): + doc = _doc_text() + assert "issue #65064" in doc + assert "list index out of range" in doc From cad03f59816283bffbb26e74bf43494f3d024080 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 25 Jun 2026 16:30:46 -0700 Subject: [PATCH 7/7] Mark SoftLayer cloud drivers as deprecated --- changelog/56546.deprecated.md | 1 + doc/topics/cloud/softlayer.rst | 21 ++++++++++ salt/cloud/clouds/softlayer.py | 7 ++++ salt/cloud/clouds/softlayer_hw.py | 7 ++++ .../cloud/clouds/test_softlayer_options.py | 38 +++++++++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 changelog/56546.deprecated.md create mode 100644 tests/pytests/unit/cloud/clouds/test_softlayer_options.py diff --git a/changelog/56546.deprecated.md b/changelog/56546.deprecated.md new file mode 100644 index 000000000000..090572b523e6 --- /dev/null +++ b/changelog/56546.deprecated.md @@ -0,0 +1 @@ +Marked the ``softlayer`` and ``softlayer_hw`` cloud drivers as deprecated and scheduled for removal in a future Salt release. SoftLayer was acquired by IBM and rebranded as IBM Cloud Classic Infrastructure; these drivers are no longer exercised by Salt's test suite. The driver source docstrings and the *Getting Started With SoftLayer* doc page now carry ``.. deprecated::`` banners that link to issue #56546 and to recommended alternatives (Saltify, the IBM Cloud Terraform provider). diff --git a/doc/topics/cloud/softlayer.rst b/doc/topics/cloud/softlayer.rst index 2aa077874902..70ae1b748d7c 100644 --- a/doc/topics/cloud/softlayer.rst +++ b/doc/topics/cloud/softlayer.rst @@ -1,7 +1,28 @@ +.. _cloud-getting-started-softlayer: + ============================== Getting Started With SoftLayer ============================== +.. deprecated:: 3006.0 + + The ``softlayer`` and ``softlayer_hw`` cloud drivers are deprecated and + will be removed in a future Salt release. SoftLayer was acquired by IBM + and rebranded as IBM Cloud Classic Infrastructure; the Python + `SoftLayer `_ SDK these drivers + depend on continues to publish releases but is in maintenance mode. + + These drivers are not exercised in Salt's automated test suite. Users + starting new SoftLayer / IBM Cloud Classic deployments should manage + instances with the IBM Cloud CLI or the + `IBM Cloud Provider for Terraform + `_ and + register existing minions with the Salt master directly, or use the + :ref:`Saltify ` driver. + + See `issue #56546 `_ + for the discussion that led to deprecation. + SoftLayer is a public cloud host, and baremetal hardware hosting service. Dependencies diff --git a/salt/cloud/clouds/softlayer.py b/salt/cloud/clouds/softlayer.py index c0f282f84ed4..0911898cdaa4 100644 --- a/salt/cloud/clouds/softlayer.py +++ b/salt/cloud/clouds/softlayer.py @@ -2,6 +2,13 @@ SoftLayer Cloud Module ====================== +.. deprecated:: 3006.0 + + The ``softlayer`` driver is deprecated and scheduled for removal in a + future Salt release. See + :ref:`Getting Started With SoftLayer ` + and issue #56546 for details and recommended alternatives. + The SoftLayer cloud module is used to control access to the SoftLayer VPS system. diff --git a/salt/cloud/clouds/softlayer_hw.py b/salt/cloud/clouds/softlayer_hw.py index f8a92f8a8a8c..c4cf3ae6f7d8 100644 --- a/salt/cloud/clouds/softlayer_hw.py +++ b/salt/cloud/clouds/softlayer_hw.py @@ -2,6 +2,13 @@ SoftLayer HW Cloud Module ========================= +.. deprecated:: 3006.0 + + The ``softlayer_hw`` driver is deprecated and scheduled for removal in + a future Salt release. See + :ref:`Getting Started With SoftLayer ` + and issue #56546 for details and recommended alternatives. + The SoftLayer HW cloud module is used to control access to the SoftLayer hardware cloud system diff --git a/tests/pytests/unit/cloud/clouds/test_softlayer_options.py b/tests/pytests/unit/cloud/clouds/test_softlayer_options.py new file mode 100644 index 000000000000..292876e37f92 --- /dev/null +++ b/tests/pytests/unit/cloud/clouds/test_softlayer_options.py @@ -0,0 +1,38 @@ +""" +Documentation-tracking tests for the SoftLayer driver. + +Backed by issue #56546: the SoftLayer / softlayer_hw drivers are slated for +removal in a future release. The driver source and the user-facing docs must +both carry a ``.. deprecated::`` block so users are not surprised when the +driver is removed. +""" + +import pathlib + +from salt.cloud.clouds import softlayer, softlayer_hw + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[5] +SOFTLAYER_DOC = REPO_ROOT / "doc" / "topics" / "cloud" / "softlayer.rst" + + +def _doc_text(): + return SOFTLAYER_DOC.read_text(encoding="utf-8") + + +def test_softlayer_doc_has_deprecation_banner(): + doc = _doc_text() + assert ".. deprecated:: 3006.0" in doc + assert "softlayer" in doc and "softlayer_hw" in doc + + +def test_softlayer_doc_links_to_issue(): + doc = _doc_text() + assert "issue #56546" in doc + + +def test_softlayer_module_docstring_marks_deprecated(): + assert ".. deprecated:: 3006.0" in (softlayer.__doc__ or "") + + +def test_softlayer_hw_module_docstring_marks_deprecated(): + assert ".. deprecated:: 3006.0" in (softlayer_hw.__doc__ or "")