diff --git a/changelog/61245.added b/changelog/61245.added new file mode 100644 index 00000000000..83b0b1ae60d --- /dev/null +++ b/changelog/61245.added @@ -0,0 +1 @@ +Added node label support for GCE diff --git a/doc/topics/cloud/gce.rst b/doc/topics/cloud/gce.rst index 8a051dfa0d8..e7c9283bcd2 100644 --- a/doc/topics/cloud/gce.rst +++ b/doc/topics/cloud/gce.rst @@ -147,6 +147,7 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or location: europe-west1-b network: default subnetwork: default + labels: '{"name": "myinstance"}' tags: '["one", "two", "three"]' metadata: '{"one": "1", "2": "two"}' use_persistent_disk: True @@ -192,6 +193,7 @@ Set up an initial profile at ``/etc/salt/cloud.profiles`` or location: europe-west1-b network: default subnetwork: default + labels: '{"name": "myinstance"}' tags: '["one", "two", "three"]' metadata: '{"one": "1", "2": "two"}' use_persistent_disk: True @@ -235,6 +237,15 @@ Additionally, the subnetwork your instance is created under is associated with t .. versionadded:: 2017.7.0 +labels +------ + +This setting allows you to set labels on your GCE instances. It +should be a dictionary and must be parse-able by the python +ast.literal_eval() function to convert it to a python dictionary. + +.. versionadded:: 3006 + tags ---- @@ -322,6 +333,7 @@ key in your cloud profile. The following example enables the bigquery scope. location: us-central1-a network: default subnetwork: default + labels: '{"name": "myinstance"}' tags: '["one", "two", "three"]' metadata: '{"one": "1", "2": "two", "sshKeys": ""}' diff --git a/salt/cloud/clouds/gce.py b/salt/cloud/clouds/gce.py index dafe6def7eb..fb18ecb7a49 100644 --- a/salt/cloud/clouds/gce.py +++ b/salt/cloud/clouds/gce.py @@ -393,6 +393,24 @@ def __get_size(conn, vm_): return conn.ex_get_size(size, __get_location(conn, vm_)) +def __get_labels(vm_): + """ + Get configured labels. + """ + l = config.get_cloud_config_value( + "ex_labels", vm_, __opts__, default="{}", search_global=False + ) + # Consider warning the user that the labels in the cloud profile + # could not be interpreted, bad formatting? + try: + labels = literal_eval(l) + except Exception: # pylint: disable=W0703 + labels = None + if not labels or not isinstance(labels, dict): + labels = None + return labels + + def __get_tags(vm_): """ Get configured tags. @@ -2320,6 +2338,7 @@ def request_instance(vm_): "size": __get_size(conn, vm_), "image": __get_image(conn, vm_), "location": __get_location(conn, vm_), + "ex_labels": __get_labels(vm_), "ex_network": __get_network(conn, vm_), "ex_subnetwork": __get_subnetwork(vm_), "ex_tags": __get_tags(vm_), diff --git a/tests/pytests/unit/cloud/clouds/test_gce.py b/tests/pytests/unit/cloud/clouds/test_gce.py index 9253f3746b6..784c83c204e 100644 --- a/tests/pytests/unit/cloud/clouds/test_gce.py +++ b/tests/pytests/unit/cloud/clouds/test_gce.py @@ -49,26 +49,153 @@ def configure_loader_modules(): } -@pytest.fixture(scope="module") -def location(): - return collections.namedtuple("Location", "name")("chicago") +@pytest.fixture( + params=[ + {"expected": "", "image": ""}, + {"expected": None, "image": None}, + {"expected": "debian-10", "image": "debian-10"}, + ] +) +def config_image(request): + return request.param["expected"], request.param["image"] + + +@pytest.fixture( + params=[ + {"expected": None, "label": "{}"}, + {"expected": {"mylabel": "myvalue"}, "label": "{'mylabel': 'myvalue'}"}, + ] +) +def config_labels(request): + return request.param["expected"], request.param["label"] + + +@pytest.fixture( + params=[ + { + "expected": collections.namedtuple("Location", "name")("chicago"), + "location": collections.namedtuple("Location", "name")("chicago"), + }, + ] +) +def config_location(request): + return request.param["expected"], request.param["location"] + + +@pytest.fixture( + params=[ + { + "expected": {"items": [{"key": "salt-cloud-profile", "value": None}]}, + "metadata": {}, + }, + { + "expected": { + "items": [ + {"key": "mykey", "value": "myvalue"}, + {"key": "salt-cloud-profile", "value": None}, + ] + }, + "metadata": "{'mykey': 'myvalue'}", + }, + ] +) +def config_metadata(request): + return request.param["expected"], request.param["metadata"] + + +@pytest.fixture( + params=[ + {"expected": "mynetwork", "network": "mynetwork"}, + ] +) +def config_network(request): + return request.param["expected"], request.param["network"] + + +@pytest.fixture( + params=[ + {"expected": "e2-standard-2", "size": "e2-standard-2"}, + ] +) +def config_size(request): + return request.param["expected"], request.param["size"] + + +@pytest.fixture( + params=[ + {"expected": "mysubnetwork", "subnetwork": "mysubnetwork"}, + ] +) +def config_subnetwork(request): + return request.param["expected"], request.param["subnetwork"] + + +@pytest.fixture( + params=[ + {"expected": None, "tag": "{}"}, + {"expected": ["mytag", "myvalue"], "tag": "['mytag', 'myvalue']"}, + ] +) +def config_tags(request): + return request.param["expected"], request.param["tag"] @pytest.fixture -def config(location): - - return { +def config( + config_image, + config_labels, + config_location, + config_metadata, + config_network, + config_size, + config_subnetwork, + config_tags, +): + expected_image, image = config_image + expected_labels, labels = config_labels + expected_location, location = config_location + expected_metadata, metadata = config_metadata + expected_network, network = config_network + expected_size, size = config_size + expected_subnetwork, subnetwork = config_subnetwork + expected_tags, tags = config_tags + expected_call_kwargs = { + "ex_disk_type": "pd-standard", + "ex_metadata": expected_metadata, + "ex_accelerator_count": 42, + "name": "new", + "ex_service_accounts": None, + "external_ip": "ephemeral", + "ex_accelerator_type": "foo", + "ex_tags": expected_tags, + "ex_labels": expected_labels, + "ex_disk_auto_delete": True, + "ex_network": expected_network, + "ex_disks_gce_struct": None, + "ex_preemptible": False, + "ex_can_ip_forward": False, + "ex_on_host_maintenance": "TERMINATE", + "location": expected_location, + "ex_subnetwork": expected_subnetwork, + "image": expected_image, + "size": expected_size, + } + config = { "name": "new", "driver": "gce", "profile": None, - "size": 1234, - "image": "myimage", + "size": size, + "image": image, "location": location, - "ex_network": "mynetwork", - "ex_subnetwork": "mysubnetwork", - "ex_tags": "mytags", - "ex_metadata": "metadata", + "ex_accelerator_type": "foo", + "ex_accelerator_count": 42, + "network": network, + "subnetwork": subnetwork, + "ex_labels": labels, + "tags": tags, + "metadata": metadata, } + return expected_call_kwargs, config @pytest.fixture @@ -195,36 +322,13 @@ def test_get_configured_provider_should_return_expected_result(fake_conf_provide assert actual_result is expected_result -def test_request_instance_with_accelerator(config, location, conn, fake_libcloud_2_5_0): +def test_request_instance_with_accelerator(config, conn): """ Test requesting an instance with GCE accelerators """ - - config.update({"ex_accelerator_type": "foo", "ex_accelerator_count": 42}) - call_kwargs = { - "ex_disk_type": "pd-standard", - "ex_metadata": {"items": [{"value": None, "key": "salt-cloud-profile"}]}, - "ex_accelerator_count": 42, - "name": "new", - "ex_service_accounts": None, - "external_ip": "ephemeral", - "ex_accelerator_type": "foo", - "ex_tags": None, - "ex_disk_auto_delete": True, - "ex_network": "default", - "ex_disks_gce_struct": None, - "ex_preemptible": False, - "ex_can_ip_forward": False, - "ex_on_host_maintenance": "TERMINATE", - "location": location, - "ex_subnetwork": None, - "image": "myimage", - "size": 1234, - } - - gce.request_instance(config) - - conn.create_node.assert_called_once_with(**call_kwargs) + expected_call_kwargs, vm_config = config + gce.request_instance(vm_config) + conn.create_node.assert_called_once_with(**expected_call_kwargs) def test_create_address_should_fire_creating_and_created_events_with_expected_args(