Skip to content

Commit

Permalink
reimplement cloudinit's rand_user_password
Browse files Browse the repository at this point in the history
With the goal of removing cloud-init from the subiquity snap,
we can reimplement cloudinit.config.cc_set_passwords.rand_user_password
in subiquity. Implementation lifted from cloud-init repo [1] to match
behavior.

[1] https://github.com/canonical/cloud-init/blob/6e4153b346bc0d3f3422c01a3f93ecfb28269da2/cloudinit/config/cc_set_passwords.py#L249
  • Loading branch information
Chris-Peterson444 committed Jul 19, 2024
1 parent 4f3ce30 commit 047a0b1
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 2 deletions.
22 changes: 21 additions & 1 deletion subiquity/cloudinit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import asyncio
import json
import logging
import random
import re
from collections.abc import Awaitable
from collections.abc import Awaitable, Sequence
from string import ascii_letters, digits
from subprocess import CompletedProcess
from typing import Optional

Expand All @@ -13,6 +15,11 @@

log = logging.getLogger("subiquity.cloudinit")

# We are removing certain 'painful' letters/numbers
# Set copied from cloud-init
# https://github.com/canonical/cloud-init/blob/6e4153b346bc0d3f3422c01a3f93ecfb28269da2/cloudinit/config/cc_set_passwords.py#L33 # noqa: E501
CLOUD_INIT_PW_SET = "".join([x for x in ascii_letters + digits if x not in "loLOI01"])


class CloudInitSchemaValidationError(NonReportableException):
"""Exception for cloud config schema validation failure.
Expand Down Expand Up @@ -153,3 +160,16 @@ async def validate_cloud_init_schema() -> None:
raise CloudInitSchemaValidationError(keys=causes)

return None


def rand_str(strlen: int = 32, select_from: Optional[Sequence] = None) -> str:
r: random.SystemRandom = random.SystemRandom()
if not select_from:
select_from: str = ascii_letters + digits
return "".join([r.choice(select_from) for _x in range(strlen)])


# Generate random user passwords the same way cloud-init does
# https://github.com/canonical/cloud-init/blob/6e4153b346bc0d3f3422c01a3f93ecfb28269da2/cloudinit/config/cc_set_passwords.py#L249 # noqa: E501
def rand_user_password(pwlen: int = 20) -> str:
return rand_str(strlen=pwlen, select_from=CLOUD_INIT_PW_SET)
2 changes: 1 addition & 1 deletion subiquity/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
import jsonschema
import yaml
from aiohttp import web
from cloudinit.config.cc_set_passwords import rand_user_password
from jsonschema.exceptions import ValidationError
from systemd import journal

from subiquity.cloudinit import (
CloudInitSchemaValidationError,
cloud_init_status_wait,
get_host_combined_cloud_config,
rand_user_password,
validate_cloud_init_schema,
)
from subiquity.common.api.server import bind, controller_for_request
Expand Down
33 changes: 33 additions & 0 deletions subiquity/tests/test_cloudinit.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
from unittest.mock import Mock, patch

from subiquity.cloudinit import (
CLOUD_INIT_PW_SET,
CloudInitSchemaValidationError,
cloud_init_status_wait,
cloud_init_version,
get_schema_failure_keys,
rand_str,
rand_user_password,
read_json_extended_status,
read_legacy_status,
supports_format_json,
Expand Down Expand Up @@ -215,3 +218,33 @@ async def test_get_schema_warn_on_timeout(self, log_mock, wait_for_mock):
sources = await get_schema_failure_keys()
log_mock.warning.assert_called()
self.assertEqual([], sources)


class TestCloudInitRandomStrings(SubiTestCase):
def test_passwd_constraints(self):
# password is 20 characters by default
password = rand_user_password()
self.assertEqual(len(password), 20)

# password is requested length
password = rand_user_password(pwlen=32)
self.assertEqual(len(password), 32)

# passwords contain valid chars
# sample passwords
for _i in range(100):
password = rand_user_password()
self.assertTrue(all(char in CLOUD_INIT_PW_SET for char in password))

def test_rand_string_generation(self):
# random string is 32 characters by default
password = rand_str()
self.assertEqual(len(password), 32)

# password is requested length
password = rand_str(strlen=20)
self.assertEqual(len(password), 20)

# password characters sampled from provided set
choices = ["a"]
self.assertEqual("a" * 32, rand_str(select_from=choices))

0 comments on commit 047a0b1

Please sign in to comment.