From 44f3d63973b1acd59c5e974bb170003e9cec2e4f Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Mon, 15 Nov 2021 15:50:27 -0800 Subject: [PATCH] Add admin password support for Azure driver Under Azure, an admin password is required in order to launch a VM from a Windows image. Add support for that. Also, shorten the node name to less than 15 characters in order to accomodate Windows restrictions. Change-Id: I899f3e02046ffdb5f9fd19fe90c4bc9afdb01a7c --- doc/source/azure.rst | 18 ++++++ nodepool/driver/azure/adapter.py | 29 +++++++-- nodepool/driver/azure/config.py | 6 ++ nodepool/driver/statemachine.py | 3 +- nodepool/tests/fixtures/azure.yaml | 28 ++++++++ nodepool/tests/unit/test_driver_azure.py | 64 +++++++++++++++++++ .../azure-password-c70896bf49deab8a.yaml | 5 ++ 7 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/azure-password-c70896bf49deab8a.yaml diff --git a/doc/source/azure.rst b/doc/source/azure.rst index 33104f946..4c66da36f 100644 --- a/doc/source/azure.rst +++ b/doc/source/azure.rst @@ -248,6 +248,24 @@ section of the configuration. The username that should be used when connecting to the node. + .. attr:: password + :type: str + + If booting a Windows image, an administrative password is + required. Either supply it here, or set + :attr:`providers.[azure].cloud-images.generate-password`. + Nodepool does not provide the password to requesting clients; + to be used it must be provided in some other manner. + + .. attr:: generate-password + :type: bool + + If booting a Windows image, an administrative password is + required. If the password is not actually used (e.g., the + image has key-based authentication enabled), a random + password can be provided by enabling this option. The + password is not stored anywhere and is not retrievable. + .. attr:: key :type: str diff --git a/nodepool/driver/azure/adapter.py b/nodepool/driver/azure/adapter.py index 916710401..1a2799f3c 100644 --- a/nodepool/driver/azure/adapter.py +++ b/nodepool/driver/azure/adapter.py @@ -12,10 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. -import os -import math -import logging import json +import logging +import math +import os +import random +import string import cachetools.func @@ -41,6 +43,18 @@ def quota_info_from_sku(sku): instances=1) +def generate_password(): + while True: + chars = random.choices(string.ascii_lowercase + + string.ascii_uppercase + + string.digits, + k=64) + if ((set(string.ascii_lowercase) & set(chars)) and + (set(string.ascii_uppercase) & set(chars)) and + (set(string.digits) & set(chars))): + return(''.join(chars)) + + class AzureInstance(statemachine.Instance): def __init__(self, vm, nic=None, public_ipv4=None, public_ipv6=None, sku=None): @@ -646,7 +660,7 @@ class AzureAdapter(statemachine.Adapter): else: image_reference = {'id': label.cloud_image.image_id} os_profile = {'computerName': hostname} - if image.username and image.key: + if image.key: linux_config = { 'ssh': { 'publicKeys': [{ @@ -657,8 +671,13 @@ class AzureAdapter(statemachine.Adapter): }, "disablePasswordAuthentication": True, } - os_profile['adminUsername'] = image.username os_profile['linuxConfiguration'] = linux_config + if image.username: + os_profile['adminUsername'] = image.username + if image.password: + os_profile['adminPassword'] = image.password + elif image.generate_password: + os_profile['adminPassword'] = generate_password() if label.custom_data: os_profile['customData'] = label.custom_data diff --git a/nodepool/driver/azure/config.py b/nodepool/driver/azure/config.py index a400d0264..8b32e50ff 100644 --- a/nodepool/driver/azure/config.py +++ b/nodepool/driver/azure/config.py @@ -32,6 +32,8 @@ class AzureProviderCloudImage(ConfigValue): } self.name = image['name'] self.username = image['username'] + self.password = image.get('password') + self.generate_password = image.get('generate-password', False) # TODO(corvus): remove zuul_public_key self.key = image.get('key', zuul_public_key) self.image_reference = image.get('image-reference') @@ -60,6 +62,8 @@ class AzureProviderCloudImage(ConfigValue): return v.All({ v.Required('name'): str, v.Required('username'): str, + 'password': str, + 'generate-password': bool, # TODO(corvus): make required when zuul_public_key removed 'key': str, v.Exclusive('image-reference', 'spec'): azure_image_reference, @@ -89,6 +93,8 @@ class AzureProviderDiskImage(ConfigValue): self.python_path = image.get('python-path') self.shell_type = image.get('shell-type') self.username = image.get('username') + self.password = image.get('password') + self.generate_password = image.get('generate-password', False) self.key = image.get('key') self.connection_type = image.get('connection-type', 'ssh') self.connection_port = image.get( diff --git a/nodepool/driver/statemachine.py b/nodepool/driver/statemachine.py index 210267a7a..36f59229a 100644 --- a/nodepool/driver/statemachine.py +++ b/nodepool/driver/statemachine.py @@ -122,7 +122,8 @@ class StateMachineNodeLauncher(stats.StatsReporter): self.node.connection_type = image.connection_type self.zk.storeNode(self.node) - hostname = 'nodepool-' + self.node.id + # Windows computer names can be no more than 15 chars long. + hostname = 'np' + self.node.id retries = self.manager.provider.launch_retries metadata = {'nodepool_node_id': self.node.id, 'nodepool_pool_name': self.handler.pool.name, diff --git a/nodepool/tests/fixtures/azure.yaml b/nodepool/tests/fixtures/azure.yaml index 0ef0c65f5..64201d70f 100644 --- a/nodepool/tests/fixtures/azure.yaml +++ b/nodepool/tests/fixtures/azure.yaml @@ -15,6 +15,10 @@ zookeeper-tls: labels: - name: bionic min-ready: 0 + - name: windows-password + min-ready: 0 + - name: windows-generate + min-ready: 0 providers: - name: azure @@ -34,6 +38,22 @@ providers: publisher: Canonical version: latest offer: UbuntuServer + - name: windows-password + image-reference: + sku: 2022-datacenter-azure-edition + publisher: MicrosoftWindowsServer + version: latest + offer: WindowsServer + username: foobar + password: reallybadpassword123 + - name: windows-generate + image-reference: + sku: 2022-datacenter-azure-edition + publisher: MicrosoftWindowsServer + version: latest + offer: WindowsServer + username: foobar + generate-password: True pools: - name: main max-servers: 10 @@ -51,3 +71,11 @@ providers: systemPurpose: CI user-data: "This is the user data" custom-data: "This is the custom data" + - name: windows-password + cloud-image: windows-password + hardware-profile: + vm-size: Standard_B1ls + - name: windows-generate + cloud-image: windows-generate + hardware-profile: + vm-size: Standard_B1ls diff --git a/nodepool/tests/unit/test_driver_azure.py b/nodepool/tests/unit/test_driver_azure.py index a42c97cf0..78ec74ed4 100644 --- a/nodepool/tests/unit/test_driver_azure.py +++ b/nodepool/tests/unit/test_driver_azure.py @@ -139,3 +139,67 @@ class TestDriverAzure(tests.DBTestCase): "/subscriptions/c35cf7df-ed75-4c85-be00-535409a85120" "/resourceGroups/nodepool/providers/Microsoft.Compute" "/images/test-image-1234") + + def test_azure_windows_image_password(self): + configfile = self.setup_config( + 'azure.yaml', + auth_path=self.fake_azure.auth_file.name) + pool = self.useNodepool(configfile, watermark_sleep=1) + pool.start() + req = zk.NodeRequest() + req.state = zk.REQUESTED + req.node_types.append('windows-password') + + self.zk.storeNodeRequest(req) + req = self.waitForNodeRequest(req) + + self.assertEqual(req.state, zk.FULFILLED) + self.assertNotEqual(req.nodes, []) + node = self.zk.getNode(req.nodes[0]) + self.assertEqual(node.allocated_to, req.id) + self.assertEqual(node.state, zk.READY) + self.assertIsNotNone(node.launcher) + self.assertEqual(node.connection_type, 'ssh') + self.assertEqual(node.attributes, + {'key1': 'value1', 'key2': 'value2'}) + self.assertEqual(node.host_keys, ['ssh-rsa FAKEKEY']) + self.assertEqual( + self.fake_azure.crud['Microsoft.Compute/virtualMachines']. + requests[0]['properties']['osProfile']['adminUsername'], + 'foobar') + self.assertEqual( + self.fake_azure.crud['Microsoft.Compute/virtualMachines']. + requests[0]['properties']['osProfile']['adminPassword'], + 'reallybadpassword123') + + def test_azure_windows_image_generate(self): + configfile = self.setup_config( + 'azure.yaml', + auth_path=self.fake_azure.auth_file.name) + pool = self.useNodepool(configfile, watermark_sleep=1) + pool.start() + req = zk.NodeRequest() + req.state = zk.REQUESTED + req.node_types.append('windows-generate') + + self.zk.storeNodeRequest(req) + req = self.waitForNodeRequest(req) + + self.assertEqual(req.state, zk.FULFILLED) + self.assertNotEqual(req.nodes, []) + node = self.zk.getNode(req.nodes[0]) + self.assertEqual(node.allocated_to, req.id) + self.assertEqual(node.state, zk.READY) + self.assertIsNotNone(node.launcher) + self.assertEqual(node.connection_type, 'ssh') + self.assertEqual(node.attributes, + {'key1': 'value1', 'key2': 'value2'}) + self.assertEqual(node.host_keys, ['ssh-rsa FAKEKEY']) + self.assertEqual( + self.fake_azure.crud['Microsoft.Compute/virtualMachines']. + requests[0]['properties']['osProfile']['adminUsername'], + 'foobar') + self.assertEqual( + len(self.fake_azure.crud['Microsoft.Compute/virtualMachines']. + requests[0]['properties']['osProfile']['adminPassword']), + 64) diff --git a/releasenotes/notes/azure-password-c70896bf49deab8a.yaml b/releasenotes/notes/azure-password-c70896bf49deab8a.yaml new file mode 100644 index 000000000..fd85dd28a --- /dev/null +++ b/releasenotes/notes/azure-password-c70896bf49deab8a.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The Azure driver now supports setting an admin password, which is + required in order to launch Windows images on Azure.