Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cloudbaseinit/conf/cloudstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(self, config):
super(CloudStackOptions, self).__init__(config, group="cloudstack")
self._options = [
cfg.StrOpt(
"metadata_base_url", default="http://10.1.1.1/",
"metadata_base_url", default="http://data-server/",
help="The base URL where the service looks for metadata",
deprecated_name="cloudstack_metadata_ip",
deprecated_group="DEFAULT"),
Expand All @@ -46,6 +46,10 @@ def __init__(self, config):
cfg.BoolOpt(
"add_metadata_private_ip_route", default=False,
help="Add a route for the metadata ip address to the gateway"),
cfg.StrOpt(
"disk_label", default="config-2",
help="Disk label of ConfigDrive"
)
]

def register(self):
Expand Down
1 change: 1 addition & 0 deletions cloudbaseinit/conf/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ def __init__(self, config):
'metadata_services',
default=[
'cloudbaseinit.metadata.services.httpservice.HttpService',
'cloudbaseinit.metadata.services.cloudstack.ConfigDrive',
'cloudbaseinit.metadata.services'
'.configdrive.ConfigDriveService',
'cloudbaseinit.metadata.services.ec2service.EC2Service',
Expand Down
3 changes: 3 additions & 0 deletions cloudbaseinit/metadata/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ def get_admin_username(self):
def get_admin_password(self):
pass

def confirm_admin_password(self, password):
"""Mark the admin password as applied (no-op by default)."""

@property
def can_post_password(self):
return False
Expand Down
94 changes: 83 additions & 11 deletions cloudbaseinit/metadata/services/cloudstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# under the License.

import contextlib
import hashlib
import http.client
import posixpath
import urllib
Expand All @@ -21,6 +22,8 @@

from cloudbaseinit import conf as cloudbaseinit_conf
from cloudbaseinit.metadata.services import base
from cloudbaseinit.metadata.services import baseconfigdrive
from cloudbaseinit.metadata.services import baseopenstackservice
from cloudbaseinit.osutils import factory as osutils_factory
from cloudbaseinit.utils import encoding
from cloudbaseinit.utils import network
Expand All @@ -33,9 +36,9 @@
TIMEOUT = 10


class CloudStack(base.BaseHTTPMetadataService):
class DataServer(base.BaseHTTPMetadataService):

"""Metadata service for Apache CloudStack.
"""Metadata service based on DataServer for Apache CloudStack.

Apache CloudStack is an open source software designed to deploy and
manage large networks of virtual machines, as a highly available,
Expand All @@ -44,7 +47,7 @@ class CloudStack(base.BaseHTTPMetadataService):
"""

def __init__(self):
super(CloudStack, self).__init__(
super(DataServer, self).__init__(
# Note(alexcoman): The base url used by the current metadata
# service will be updated later by the `_test_api` method.
base_url=None,
Expand Down Expand Up @@ -83,7 +86,7 @@ def _test_api(self, metadata_url):

def load(self):
"""Obtain all the required information."""
super(CloudStack, self).load()
super(DataServer, self).load()

if CONF.cloudstack.add_metadata_private_ip_route:
network.check_metadata_ip_route(CONF.cloudstack.metadata_base_url)
Expand Down Expand Up @@ -240,16 +243,14 @@ def _delete_password(self):
def get_admin_password(self):
"""Get the admin password from the Password Server.

.. note:
The password is deleted from the Password Server after the first
call of this method.
Another request for password will work only if the password was
changed and sent to the Password Server.
The password is deleted only after confirm_admin_password().
"""
password = self._get_password()
return self._get_password()

def confirm_admin_password(self, password):
"""Delete the password from the Password Server."""
if password:
self._delete_password()
return password

@property
def can_update_password(self):
Expand All @@ -259,3 +260,74 @@ def can_update_password(self):
def is_password_changed(self):
"""Check if a new password exists in the Password Server."""
return bool(self._get_password())


# For backward compatibility, CloudStack Class is an alias to DataServer Class
CloudStack = DataServer


class ConfigDrive(baseconfigdrive.BaseConfigDriveService,
baseopenstackservice.BaseOpenStackService):

def __init__(self):
# CloudStack marker rejects plain OpenStack config drives early.
super(ConfigDrive, self).__init__(
CONF.cloudstack.disk_label,
'cloudstack\\metadata\\instance-id.txt')

def _preprocess_options(self):
self._searched_types = set(['iso'])
self._searched_locations = set(['cdrom'])

def get_instance_id(self):
return self._get_cache_data(
posixpath.join('cloudstack', 'metadata', 'instance-id.txt'),
decode=True).strip()

@staticmethod
def _password_hash(password):
return hashlib.sha256(password.encode('utf-8')).hexdigest()

def _persist_password_hash(self, password):
osutils = osutils_factory.get_os_utils()
osutils.set_config_value(
'PasswordHash', self._password_hash(password),
self.get_instance_id())

def _get_password(self):
path = posixpath.normpath(
posixpath.join('cloudstack', 'password', 'vm_password.txt'))
try:
password = self._get_cache_data(path, decode=True).strip()
except base.NotExistingMetadataException:
LOG.info('No password file was found in ConfigDrive')
return None

if not password or password == SAVED_PASSWORD:
LOG.info('No password available in ConfigDrive')
return None

LOG.info('Password file was found in ConfigDrive')
return password

def get_admin_password(self):
return self._get_password()

def confirm_admin_password(self, password):
self._persist_password_hash(password)

@property
def can_update_password(self):
return True

def is_password_changed(self):
password = self._get_password()
if not password:
return False
osutils = osutils_factory.get_os_utils()
old_password_hash = osutils.get_config_value(
'PasswordHash', self.get_instance_id())
if old_password_hash != self._password_hash(password):
LOG.debug('New password is detected')
return True
return False
2 changes: 1 addition & 1 deletion cloudbaseinit/metadata/services/osconfigdrive/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def _meta_data_file_exists(self, drive, metadata_file):
def _check_for_config_drive(self, drive, required_drive_label,
metadata_file):
label = self._osutils.get_volume_label(drive)
if label and label.lower() == required_drive_label and \
if label and label.lower() == required_drive_label.lower() and \
self._meta_data_file_exists(drive, metadata_file):
LOG.info('Config Drive found on %s', drive)
return True
Expand Down
8 changes: 8 additions & 0 deletions cloudbaseinit/plugins/common/setuserpassword.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ def _set_password(self, service, osutils, user_name, shared_data):
CONF.user_password_length)

osutils.set_user_password(user_name, password)
if injected:
service.confirm_admin_password(password)
elif service.can_update_password:
# Consume metadata password even when not injected, to avoid
# resetting the guest password on every boot.
metadata_password = service.get_admin_password()
if metadata_password:
service.confirm_admin_password(metadata_password)
self._change_logon_behaviour(user_name, password_injected=injected)
return password

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ def test_check_for_config_drive_exists(self):
def test_check_for_config_drive_exists_upper_label(self):
self._test_check_for_config_drive(label="CONFIG-2")

@mock.patch('os.path.exists')
def test_check_for_config_drive_upper_required_label(self, mock_exists):
drive = "C:\\"
self.osutils.get_volume_label.return_value = "config-2"
mock_exists.return_value = True

response = self._config_manager._check_for_config_drive(
drive, "CONFIG-2", self._fake_metadata_file)

self.assertTrue(response)

def test_check_for_config_drive_missing(self):
self._test_check_for_config_drive(exists=False, fail=True)

Expand Down
3 changes: 3 additions & 0 deletions cloudbaseinit/tests/metadata/services/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ def test_can_update_password(self):
def test_is_password_changed(self):
self.assertFalse(self._service.is_password_changed())

def test_confirm_admin_password(self):
self.assertIsNone(self._service.confirm_admin_password('s3cret'))

@mock.patch('cloudbaseinit.metadata.services.base.'
'BaseMetadataService.get_public_keys')
def test_get_user_pwd_encryption_key(self, mock_get_public_keys):
Expand Down
145 changes: 143 additions & 2 deletions cloudbaseinit/tests/metadata/services/test_cloudstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def test_get_admin_password(self, mock_get_password, mock_delete_password):

self.assertEqual(mock.sentinel.password, password)
self.assertEqual(1, mock_get_password.call_count)
self.assertEqual(1, mock_delete_password.call_count)
self.assertFalse(mock_delete_password.called)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack.'
'_delete_password')
Expand All @@ -302,7 +302,19 @@ def test_get_admin_password_fail(self, mock_get_password,

self.assertIsNone(self._service.get_admin_password())
self.assertEqual(1, mock_get_password.call_count)
self.assertEqual(0, mock_delete_password.call_count)
self.assertFalse(mock_delete_password.called)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack.'
'_delete_password')
def test_confirm_admin_password(self, mock_delete_password):
self._service.confirm_admin_password(mock.sentinel.password)
mock_delete_password.assert_called_once_with()

@mock.patch('cloudbaseinit.metadata.services.cloudstack.CloudStack.'
'_delete_password')
def test_confirm_admin_password_none(self, mock_delete_password):
self._service.confirm_admin_password(None)
self.assertFalse(mock_delete_password.called)

def test_can_update_password(self):
self.assertTrue(self._service.can_update_password)
Expand All @@ -312,3 +324,132 @@ def test_can_update_password(self):
def test_is_password_changed(self, mock_get_password):
mock_get_password.return_value = True
self.assertTrue(self._service.is_password_changed())


class CloudStackConfigDriveTest(unittest.TestCase):

def setUp(self):
self._service = cloudstack.ConfigDrive()

def test_init_uses_cloudstack_marker(self):
self.assertEqual('config-2', self._service._drive_label)
self.assertEqual('cloudstack\\metadata\\instance-id.txt',
self._service._metadata_file)

def test_preprocess_options(self):
self._service._preprocess_options()
self.assertEqual(set(['iso']), self._service._searched_types)
self.assertEqual(set(['cdrom']), self._service._searched_locations)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_cache_data')
def test_get_instance_id(self, mock_get_cache_data):
mock_get_cache_data.return_value = 'i-123\r\n'

self.assertEqual('i-123', self._service.get_instance_id())
mock_get_cache_data.assert_called_once_with(
'cloudstack/metadata/instance-id.txt', decode=True)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_cache_data')
def test_get_password(self, mock_get_cache_data):
mock_get_cache_data.return_value = 's3cret\r\n'

self.assertEqual('s3cret', self._service._get_password())
mock_get_cache_data.assert_called_once_with(
'cloudstack/password/vm_password.txt', decode=True)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_cache_data')
def test_get_password_sentinels(self, mock_get_cache_data):
for value in ('', ' ', cloudstack.SAVED_PASSWORD,
cloudstack.SAVED_PASSWORD + '\r\n'):
mock_get_cache_data.return_value = value
self.assertIsNone(self._service._get_password())

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_cache_data')
def test_get_password_missing(self, mock_get_cache_data):
mock_get_cache_data.side_effect = base.NotExistingMetadataException()
self.assertIsNone(self._service._get_password())

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_persist_password_hash')
@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_password')
def test_get_admin_password(self, mock_get_password, mock_persist):
mock_get_password.return_value = 's3cret'

self.assertEqual('s3cret', self._service.get_admin_password())
self.assertFalse(mock_persist.called)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_persist_password_hash')
@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_password')
def test_get_admin_password_none(self, mock_get_password, mock_persist):
mock_get_password.return_value = None

self.assertIsNone(self._service.get_admin_password())
self.assertFalse(mock_persist.called)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_persist_password_hash')
def test_confirm_admin_password(self, mock_persist):
self._service.confirm_admin_password('s3cret')
mock_persist.assert_called_once_with('s3cret')

def test_can_update_password(self):
self.assertTrue(self._service.can_update_password)

@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'get_instance_id')
@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_password')
def test_is_password_changed(self, mock_get_password, mock_instance_id,
mock_get_os_utils):
mock_get_password.return_value = 's3cret'
mock_instance_id.return_value = 'i-1'
mock_osutils = mock_get_os_utils.return_value
mock_osutils.get_config_value.return_value = None

self.assertTrue(self._service.is_password_changed())
mock_osutils.get_config_value.assert_called_once_with(
'PasswordHash', 'i-1')
self.assertFalse(mock_osutils.set_config_value.called)

@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'get_instance_id')
@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_password')
def test_is_password_changed_same_hash(self, mock_get_password,
mock_instance_id,
mock_get_os_utils):
password = 's3cret'
mock_get_password.return_value = password
mock_instance_id.return_value = 'i-1'
mock_osutils = mock_get_os_utils.return_value
mock_osutils.get_config_value.return_value = (
self._service._password_hash(password))

self.assertFalse(self._service.is_password_changed())
self.assertFalse(mock_osutils.set_config_value.called)

@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'_get_password')
def test_is_password_changed_no_password(self, mock_get_password):
mock_get_password.return_value = None
self.assertFalse(self._service.is_password_changed())

@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
@mock.patch('cloudbaseinit.metadata.services.cloudstack.ConfigDrive.'
'get_instance_id')
def test_persist_password_hash(self, mock_instance_id, mock_get_os_utils):
mock_instance_id.return_value = 'i-1'
mock_osutils = mock_get_os_utils.return_value

self._service._persist_password_hash('s3cret')
mock_osutils.set_config_value.assert_called_once_with(
'PasswordHash', self._service._password_hash('s3cret'), 'i-1')
Loading