Implement cloud-config users and groups plugins
Add support for cloud-config users and groups creation. For Windows, the following format is supported: groups: - windows-group: [user1, user2] - cloud-users users: - name: brian gecos: 'Brian Cohen' primary_group: Users groups: cloud-users passwd: StrongPassw0rd inactive: False expiredate: 2020-10-01 ssh_authorized_keys: - first key - second key The passwords for Windows users are required to be in plain text. On *nix systems, the passwords are hashed. If the password is not present, a random password will be set. Fixes: https://github.com/cloudbase/cloudbase-init/issues/26 Change-Id: I035f92849a59a8370df30a6de41f66f5fb2300af
This commit is contained in:
parent
08641ae8b3
commit
6ede055475
@ -57,6 +57,10 @@ class BaseOSUtils(object):
|
||||
def rename_user(self, username, new_username):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_user_info(self, username, full_name=None,
|
||||
disabled=False, expire_interval=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def enum_users(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@ -69,6 +73,12 @@ class BaseOSUtils(object):
|
||||
def add_user_to_local_group(self, username, groupname):
|
||||
raise NotImplementedError()
|
||||
|
||||
def group_exists(self, group):
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_group(self, group, description=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_host_name(self, new_host_name):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -474,6 +474,35 @@ class WindowsUtils(base.BaseOSUtils):
|
||||
raise exception.CloudbaseInitException(
|
||||
"Renaming user failed: %s" % ex.args[2])
|
||||
|
||||
def set_user_info(self, username, full_name=None,
|
||||
disabled=False, expire_interval=None):
|
||||
|
||||
user_info = self._get_user_info(username, 2)
|
||||
|
||||
if full_name:
|
||||
user_info["full_name"] = full_name
|
||||
|
||||
if disabled:
|
||||
user_info["flags"] |= win32netcon.UF_ACCOUNTDISABLE
|
||||
else:
|
||||
user_info["flags"] &= ~win32netcon.UF_ACCOUNTDISABLE
|
||||
|
||||
if expire_interval is not None:
|
||||
user_info["acct_expires"] = int(expire_interval)
|
||||
else:
|
||||
user_info["acct_expires"] = win32netcon.TIMEQ_FOREVER
|
||||
|
||||
try:
|
||||
win32net.NetUserSetInfo(None, username, 2, user_info)
|
||||
except win32net.error as ex:
|
||||
if ex.args[0] == self.NERR_UserNotFound:
|
||||
raise exception.ItemNotFoundException(
|
||||
"User not found: %s" % username)
|
||||
else:
|
||||
LOG.debug(ex)
|
||||
raise exception.CloudbaseInitException(
|
||||
"Setting user info failed: %s" % ex.args[2])
|
||||
|
||||
def enum_users(self):
|
||||
usernames = []
|
||||
resume_handle = 0
|
||||
@ -531,6 +560,34 @@ class WindowsUtils(base.BaseOSUtils):
|
||||
raise exception.CloudbaseInitException(
|
||||
"Setting password expiration failed: %s" % ex.args[2])
|
||||
|
||||
def group_exists(self, group):
|
||||
try:
|
||||
self._get_group_info(group, 1)
|
||||
return True
|
||||
except exception.ItemNotFoundException:
|
||||
# Group not found
|
||||
return False
|
||||
|
||||
def _get_group_info(self, group, level):
|
||||
try:
|
||||
return win32net.NetLocalGroupGetInfo(None, group, level)
|
||||
except win32net.error as ex:
|
||||
if ex.args[0] == self.NERR_GroupNotFound:
|
||||
raise exception.ItemNotFoundException(
|
||||
"Group not found: %s" % group)
|
||||
else:
|
||||
raise exception.CloudbaseInitException(
|
||||
"Failed to get group info: %s" % ex.args[2])
|
||||
|
||||
def create_group(self, group, description=None):
|
||||
group_info = {"name": group}
|
||||
|
||||
try:
|
||||
win32net.NetLocalGroupAdd(None, 0, group_info)
|
||||
except win32net.error as ex:
|
||||
raise exception.CloudbaseInitException(
|
||||
"Create group failed: %s" % ex.args[2])
|
||||
|
||||
@staticmethod
|
||||
def _get_cch_referenced_domain_name(domain_name):
|
||||
return wintypes.DWORD(
|
||||
@ -562,11 +619,13 @@ class WindowsUtils(base.BaseOSUtils):
|
||||
3, ctypes.pointer(lmi), 1)
|
||||
|
||||
if ret_val == self.NERR_GroupNotFound:
|
||||
raise exception.CloudbaseInitException('Group not found')
|
||||
raise exception.CloudbaseInitException("Group '%s' not found"
|
||||
% groupname)
|
||||
elif ret_val == self.ERROR_ACCESS_DENIED:
|
||||
raise exception.CloudbaseInitException('Access denied')
|
||||
elif ret_val == self.ERROR_NO_SUCH_MEMBER:
|
||||
raise exception.CloudbaseInitException('Username not found')
|
||||
raise exception.CloudbaseInitException("Username '%s' not found"
|
||||
% username)
|
||||
elif ret_val == self.ERROR_MEMBER_IN_ALIAS:
|
||||
# The user is already a member of the group
|
||||
pass
|
||||
|
@ -32,6 +32,10 @@ PLUGINS = collections.OrderedDict([
|
||||
'cloudconfigplugins.set_hostname.SetHostnamePlugin'),
|
||||
('ntp', 'cloudbaseinit.plugins.common.userdataplugins.'
|
||||
'cloudconfigplugins.set_ntp.SetNtpPlugin'),
|
||||
('groups', 'cloudbaseinit.plugins.common.userdataplugins.'
|
||||
'cloudconfigplugins.groups.GroupsPlugin'),
|
||||
('users', 'cloudbaseinit.plugins.common.userdataplugins.'
|
||||
'cloudconfigplugins.users.UsersPlugin'),
|
||||
('runcmd', 'cloudbaseinit.plugins.common.userdataplugins.'
|
||||
'cloudconfigplugins.runcmd.RunCmdPlugin'),
|
||||
])
|
||||
|
@ -0,0 +1,77 @@
|
||||
# Copyright 2019 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
|
||||
from oslo_log import log as oslo_logging
|
||||
|
||||
from cloudbaseinit import conf as cloudbaseinit_conf
|
||||
from cloudbaseinit import exception
|
||||
from cloudbaseinit.osutils import factory as osutils_factory
|
||||
from cloudbaseinit.plugins.common.userdataplugins.cloudconfigplugins import (
|
||||
base
|
||||
)
|
||||
|
||||
CONF = cloudbaseinit_conf.CONF
|
||||
LOG = oslo_logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GroupsPlugin(base.BaseCloudConfigPlugin):
|
||||
"""Creates groups given in cloud-config for the underlying platform."""
|
||||
|
||||
def process(self, data):
|
||||
"""Process the given data received from the cloud-config userdata.
|
||||
|
||||
It knows to process only lists and dicts.
|
||||
"""
|
||||
if not isinstance(data, (list, dict)):
|
||||
raise exception.CloudbaseInitException(
|
||||
"Can't process the type of data %r" % type(data))
|
||||
|
||||
osutils = osutils_factory.get_os_utils()
|
||||
|
||||
for item in data:
|
||||
group_name = None
|
||||
group_users = []
|
||||
|
||||
if isinstance(item, six.string_types):
|
||||
group_name = item
|
||||
elif isinstance(item, dict):
|
||||
try:
|
||||
group_name = list(item.keys())[0]
|
||||
group_users = item.get(group_name, [])
|
||||
except Exception:
|
||||
LOG.error("Group details could not be parsed")
|
||||
raise
|
||||
else:
|
||||
raise exception.CloudbaseInitException(
|
||||
"Unrecognized type '%r' in group definition" % type(item))
|
||||
|
||||
if not group_name:
|
||||
LOG.warning("Group name cannot be empty")
|
||||
continue
|
||||
|
||||
try:
|
||||
if not osutils.group_exists(group_name):
|
||||
osutils.create_group(group_name)
|
||||
else:
|
||||
LOG.warning("Group '%s' already exists" % group_name)
|
||||
for group_user in group_users:
|
||||
osutils.add_user_to_local_group(group_user, group_name)
|
||||
except Exception as exc:
|
||||
raise exception.CloudbaseInitException(
|
||||
"Group '%s' could not be configured. Exception code: %s"
|
||||
% (group_name, exc))
|
||||
|
||||
return False
|
@ -0,0 +1,177 @@
|
||||
# Copyright 2019 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import pytz
|
||||
import six
|
||||
|
||||
from oslo_log import log as oslo_logging
|
||||
|
||||
from cloudbaseinit import conf as cloudbaseinit_conf
|
||||
from cloudbaseinit import exception
|
||||
from cloudbaseinit.osutils import factory as osutils_factory
|
||||
from cloudbaseinit.plugins.common.userdataplugins.cloudconfigplugins import (
|
||||
base
|
||||
)
|
||||
|
||||
CONF = cloudbaseinit_conf.CONF
|
||||
LOG = oslo_logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UsersPlugin(base.BaseCloudConfigPlugin):
|
||||
"""Creates users given in the cloud-config format."""
|
||||
|
||||
def _get_groups(self, data):
|
||||
"""Retuns all the group names that the user should be added to.
|
||||
|
||||
:rtype: list
|
||||
"""
|
||||
groups = data.get('groups', None)
|
||||
primary_group = data.get('primary_group', None)
|
||||
user_groups = []
|
||||
if isinstance(groups, six.string_types):
|
||||
user_groups.extend(groups.split(', '))
|
||||
elif isinstance(groups, (list, tuple)):
|
||||
user_groups.extend(groups)
|
||||
if isinstance(primary_group, six.string_types):
|
||||
user_groups.extend(primary_group.split(', '))
|
||||
elif isinstance(primary_group, (list, tuple)):
|
||||
user_groups.extend(primary_group)
|
||||
return user_groups
|
||||
|
||||
def _get_password(self, data, osutils):
|
||||
password = data.get('passwd', None)
|
||||
max_size = osutils.get_maximum_password_length()
|
||||
|
||||
if password and len(password) > max_size:
|
||||
raise Exception("Password has more than %d characters" % max_size)
|
||||
|
||||
if not password:
|
||||
password = osutils.generate_random_password(
|
||||
CONF.user_password_length)
|
||||
|
||||
return password
|
||||
|
||||
def _get_expire_interval(self, data):
|
||||
expiredate = data.get('expiredate', None)
|
||||
expire_interval = None
|
||||
|
||||
if isinstance(expiredate, six.string_types):
|
||||
year, month, day = map(int, expiredate.split('-'))
|
||||
expiredate = datetime.datetime(year=year, month=month, day=day,
|
||||
tzinfo=pytz.utc)
|
||||
# Py2.7 does not support timestamps, this is the
|
||||
# only way to compute the seconds passed since the unix epoch
|
||||
unix_time = datetime.datetime(year=1970, month=1, day=1,
|
||||
tzinfo=pytz.utc)
|
||||
expire_interval = (expiredate - unix_time).total_seconds()
|
||||
|
||||
return expire_interval
|
||||
|
||||
@staticmethod
|
||||
def _create_user_logon(user_name, password, osutils):
|
||||
try:
|
||||
token = osutils.create_user_logon_session(user_name,
|
||||
password)
|
||||
osutils.close_user_logon_session(token)
|
||||
except Exception:
|
||||
LOG.exception('Cannot create a user logon session for user: "%s"',
|
||||
user_name)
|
||||
|
||||
@staticmethod
|
||||
def _set_ssh_public_keys(user_name, public_keys, osutils):
|
||||
user_home = osutils.get_user_home(user_name)
|
||||
if not user_home:
|
||||
raise exception.CloudbaseInitException("User profile not found!")
|
||||
|
||||
user_ssh_dir = os.path.join(user_home, '.ssh')
|
||||
if not os.path.exists(user_ssh_dir):
|
||||
os.makedirs(user_ssh_dir)
|
||||
|
||||
authorized_keys_path = os.path.join(user_ssh_dir, "authorized_keys")
|
||||
LOG.info("Writing SSH public keys in: %s" % authorized_keys_path)
|
||||
with open(authorized_keys_path, 'w') as f:
|
||||
for public_key in public_keys:
|
||||
f.write(public_key + "\n")
|
||||
|
||||
def _create_user(self, item, osutils):
|
||||
user_name = item.get('name', None)
|
||||
password = self._get_password(item, osutils)
|
||||
user_full_name = item.get('gecos', None)
|
||||
user_expire_interval = self._get_expire_interval(item)
|
||||
user_disabled = item.get('inactive', False)
|
||||
|
||||
public_keys = item.get('ssh_authorized_keys', [])
|
||||
should_create_home = (public_keys or
|
||||
not item.get('no_create_home ', False))
|
||||
if user_disabled and should_create_home:
|
||||
raise exception.CloudbaseInitException(
|
||||
"The user is required to be enabled if public_keys "
|
||||
"or create_home are set")
|
||||
|
||||
groups = self._get_groups(item)
|
||||
|
||||
if osutils.user_exists(user_name):
|
||||
LOG.warning("User '%s' already exists " % user_name)
|
||||
osutils.set_user_password(user_name, password)
|
||||
else:
|
||||
osutils.create_user(user_name, password)
|
||||
|
||||
osutils.set_user_info(user_name, full_name=user_full_name,
|
||||
expire_interval=user_expire_interval,
|
||||
disabled=user_disabled)
|
||||
|
||||
for group in groups:
|
||||
try:
|
||||
osutils.add_user_to_local_group(user_name, group)
|
||||
except Exception:
|
||||
LOG.exception('Cannot add user "%s" to group "%s"' %
|
||||
(user_name, group))
|
||||
|
||||
if not user_disabled and should_create_home:
|
||||
self._create_user_logon(user_name, password, osutils)
|
||||
|
||||
if public_keys:
|
||||
self._set_ssh_public_keys(user_name, public_keys, osutils)
|
||||
|
||||
def process(self, data):
|
||||
"""Process the given data received from the cloud-config userdata.
|
||||
|
||||
It knows to process only lists and dicts.
|
||||
"""
|
||||
if not isinstance(data, (list, dict)):
|
||||
raise exception.CloudbaseInitException(
|
||||
"Can't process the type of data %r" % type(data))
|
||||
|
||||
osutils = osutils_factory.get_os_utils()
|
||||
for item in data:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if not {'name'}.issubset(set(item)):
|
||||
LOG.warning("Missing name key from user information %s",
|
||||
item)
|
||||
continue
|
||||
user_name = item.get('name', None)
|
||||
if not user_name:
|
||||
LOG.warning("Username cannot be empty")
|
||||
continue
|
||||
|
||||
try:
|
||||
self._create_user(item, osutils)
|
||||
except Exception as ex:
|
||||
LOG.warning("An error occurred during user '%s' creation: '%s"
|
||||
% (user_name, ex))
|
||||
|
||||
return False
|
@ -248,6 +248,45 @@ class TestWindowsUtils(testutils.CloudbaseInitTestBase):
|
||||
def test_user_does_not_exist(self):
|
||||
self._test_user_exists(exists=False)
|
||||
|
||||
@mock.patch('cloudbaseinit.osutils.windows.WindowsUtils'
|
||||
'._get_group_info')
|
||||
def _test_group_exists(self, mock_get_group_info, exists):
|
||||
fake_group_name = 'fake_group'
|
||||
if not exists:
|
||||
mock_get_group_info.side_effect = [exception.ItemNotFoundException]
|
||||
response = self._winutils.group_exists(fake_group_name)
|
||||
self.assertEqual(False, response)
|
||||
return
|
||||
response = self._winutils.group_exists(fake_group_name)
|
||||
mock_get_group_info.assert_called_once_with(fake_group_name, 1)
|
||||
self.assertEqual(True, response)
|
||||
|
||||
def test_group_exists(self):
|
||||
self._test_group_exists(exists=True)
|
||||
|
||||
def test_group_does_not_exist(self):
|
||||
self._test_group_exists(exists=False)
|
||||
|
||||
def _test_create_group(self, fail=False):
|
||||
fake_group = "fake_group"
|
||||
group_info = {"name": fake_group}
|
||||
|
||||
if fail:
|
||||
self._win32net_mock.NetLocalGroupAdd.side_effect = [
|
||||
self._win32net_mock.error(*([mock.Mock()] * 3))]
|
||||
with self.assertRaises(exception.CloudbaseInitException):
|
||||
self._winutils.create_group(fake_group)
|
||||
return
|
||||
self._winutils.create_group(fake_group)
|
||||
self._win32net_mock.NetLocalGroupAdd.assert_called_once_with(
|
||||
None, 0, group_info)
|
||||
|
||||
def test_create_group(self):
|
||||
self._test_create_group()
|
||||
|
||||
def test_create_group_fail(self):
|
||||
self._test_create_group(True)
|
||||
|
||||
def test_sanitize_shell_input(self):
|
||||
unsanitised = ' " '
|
||||
response = self._winutils.sanitize_shell_input(unsanitised)
|
||||
@ -2589,6 +2628,63 @@ class TestWindowsUtils(testutils.CloudbaseInitTestBase):
|
||||
*([mock.Mock()] * 2))
|
||||
self._test_enum_users(exc=exc)
|
||||
|
||||
@mock.patch('cloudbaseinit.osutils.windows.WindowsUtils'
|
||||
'._get_user_info')
|
||||
def _test_set_user_info(self, mock_user_info, full_name=None,
|
||||
disabled=False, expire_interval=None, exc=None):
|
||||
user_info = {
|
||||
"username": self._USERNAME,
|
||||
"full_name": 'fake user',
|
||||
"flags": 0,
|
||||
"expire_interval": self._win32netcon_mock.TIMEQ_FOREVER
|
||||
}
|
||||
if full_name:
|
||||
user_info["full_name"] = full_name
|
||||
if expire_interval:
|
||||
user_info["acct_expires"] = expire_interval
|
||||
|
||||
if disabled:
|
||||
user_info["flags"] |= self._win32netcon_mock.UF_ACCOUNTDISABLE
|
||||
else:
|
||||
user_info["flags"] &= self._win32netcon_mock.UF_ACCOUNTDISABLE
|
||||
|
||||
mock_user_info.return_value = user_info
|
||||
userset_mock = self._win32net_mock.NetUserSetInfo
|
||||
|
||||
if exc:
|
||||
userset_mock.side_effect = [exc]
|
||||
error_class = (
|
||||
exception.ItemNotFoundException if
|
||||
exc.args[0] == self._winutils.NERR_UserNotFound else
|
||||
exception.CloudbaseInitException)
|
||||
with self.assertRaises(error_class):
|
||||
self._winutils.set_user_info(self._USERNAME, full_name, True,
|
||||
expire_interval)
|
||||
return
|
||||
|
||||
self._winutils.set_user_info(self._USERNAME, full_name, True,
|
||||
expire_interval)
|
||||
userset_mock.assert_called_once_with(
|
||||
None, self._USERNAME, 2, user_info)
|
||||
|
||||
def test_set_user_info(self):
|
||||
self._test_set_user_info()
|
||||
|
||||
def test_set_user_info_full_options(self):
|
||||
self._test_set_user_info(full_name='fake_user1',
|
||||
disabled=True, expire_interval=1)
|
||||
|
||||
def test_set_user_info_not_found(self):
|
||||
exc = self._win32net_mock.error(self._winutils.NERR_UserNotFound,
|
||||
*([mock.Mock()] * 2))
|
||||
self._test_set_user_info(full_name='fake_user1',
|
||||
disabled=True, expire_interval=1,
|
||||
exc=exc)
|
||||
|
||||
def test_set_user_info_failed(self):
|
||||
exc = self._win32net_mock.error(*([mock.Mock()] * 3))
|
||||
self._test_set_user_info(exc=exc)
|
||||
|
||||
def test_enum_users(self):
|
||||
self._test_enum_users(resume_handle=False)
|
||||
|
||||
|
@ -0,0 +1,81 @@
|
||||
# Copyright 2019 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import unittest
|
||||
|
||||
try:
|
||||
import unittest.mock as mock
|
||||
except ImportError:
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from cloudbaseinit import exception
|
||||
from cloudbaseinit.plugins.common.userdataplugins.cloudconfigplugins import (
|
||||
groups
|
||||
)
|
||||
from cloudbaseinit.tests import testutils
|
||||
|
||||
CONF = cfg.CONF
|
||||
MODPATH = ("cloudbaseinit.plugins.common.userdataplugins."
|
||||
"cloudconfigplugins.groups")
|
||||
|
||||
|
||||
class GroupsPluginTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.groups_plugin = groups.GroupsPlugin()
|
||||
|
||||
@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
|
||||
def test_process_group_empty(self, mock_get_os_utils):
|
||||
fake_data = ['']
|
||||
|
||||
with testutils.LogSnatcher(MODPATH) as snatcher:
|
||||
res = self.groups_plugin.process(fake_data)
|
||||
self.assertEqual(['Group name cannot be empty'], snatcher.output)
|
||||
self.assertEqual(res, False)
|
||||
|
||||
def test_process_group_wrong_content(self):
|
||||
fake_data = 'fake_group'
|
||||
|
||||
with self.assertRaises(exception.CloudbaseInitException) as cm:
|
||||
self.groups_plugin.process(fake_data)
|
||||
expected = "Can't process the type of data %s" % type(fake_data)
|
||||
self.assertEqual(expected, str(cm.exception))
|
||||
|
||||
@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
|
||||
def test_process_group(self, mock_get_os_utils):
|
||||
fake_data = [{'group1': ['usr1', 'usr2']}]
|
||||
mock_os_util = mock.MagicMock()
|
||||
mock_os_util.add_user_to_local_group.return_value = True
|
||||
mock_os_util.group_exists.return_value = True
|
||||
mock_get_os_utils.return_value = mock_os_util
|
||||
|
||||
with testutils.LogSnatcher(MODPATH) as snatcher:
|
||||
res = self.groups_plugin.process(fake_data)
|
||||
self.assertEqual(["Group 'group1' already exists"], snatcher.output)
|
||||
self.assertEqual(res, False)
|
||||
|
||||
@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
|
||||
def test_process_group_fail(self, mock_get_os_utils):
|
||||
fake_data = [{'group1': ['usr1']}]
|
||||
mock_os_util = mock.MagicMock()
|
||||
mock_os_util.create_group.return_value = True
|
||||
mock_os_util.add_user_to_local_group.side_effect = Exception
|
||||
mock_os_util.group_exists.return_value = False
|
||||
mock_get_os_utils.return_value = mock_os_util
|
||||
|
||||
with self.assertRaises(exception.CloudbaseInitException) as cm:
|
||||
self.groups_plugin.process(fake_data)
|
||||
expected = "Group 'group1' could not be configured. Exception code: "
|
||||
self.assertEqual(expected, str(cm.exception))
|
@ -0,0 +1,124 @@
|
||||
# Copyright 2016 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import unittest
|
||||
|
||||
try:
|
||||
import unittest.mock as mock
|
||||
except ImportError:
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from cloudbaseinit import exception
|
||||
from cloudbaseinit.plugins.common.userdataplugins.cloudconfigplugins import (
|
||||
users
|
||||
)
|
||||
from cloudbaseinit.tests import testutils
|
||||
|
||||
CONF = cfg.CONF
|
||||
MODPATH = ("cloudbaseinit.plugins.common.userdataplugins."
|
||||
"cloudconfigplugins.users")
|
||||
|
||||
|
||||
class UsersPluginTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.users_plugin = users.UsersPlugin()
|
||||
|
||||
def test__get_groups(self):
|
||||
fake_data = {'groups': 'fake1, fake2', 'primary_group': 'fake3'}
|
||||
res = self.users_plugin._get_groups(fake_data)
|
||||
self.assertEqual(res, ['fake1', 'fake2', 'fake3'])
|
||||
|
||||
def test__get_password(self):
|
||||
fake_password = 'Passw0rd'
|
||||
fake_data = {"passwd": fake_password}
|
||||
mock_os_utils = mock.MagicMock()
|
||||
mock_os_utils.get_maximum_password_length.return_value = 10
|
||||
res = self.users_plugin._get_password(fake_data, mock_os_utils)
|
||||
self.assertEqual(res, fake_password)
|
||||
|
||||
def test__get_expire_interval(self):
|
||||
fake_data = {"expiredate": '2020-01-01'}
|
||||
res = self.users_plugin._get_expire_interval(fake_data)
|
||||
self.assertEqual(res, 1577836800)
|
||||
|
||||
@mock.patch(MODPATH + '.UsersPlugin._get_password')
|
||||
@mock.patch(MODPATH + '.UsersPlugin._create_user_logon')
|
||||
@mock.patch(MODPATH + '.UsersPlugin._set_ssh_public_keys')
|
||||
def test__create_user(self, mock_set_ssh_keys, mock_user_logon,
|
||||
mock_get_pass):
|
||||
fake_user = {
|
||||
'name': 'fake_user',
|
||||
'gecos': 'fake user',
|
||||
'primary_group': 'Users',
|
||||
'groups': 'test',
|
||||
'ssh_authorized_keys': ["test2", "test1"],
|
||||
'inactive': False,
|
||||
'expiredate': '3020-09-01',
|
||||
'passwd': 'Passw0rd'
|
||||
}
|
||||
mock_get_pass.return_value = 'fake_pass'
|
||||
mock_os_utils = mock.MagicMock()
|
||||
mock_os_utils.user_exists.return_value = False
|
||||
|
||||
res = self.users_plugin._create_user(fake_user, mock_os_utils)
|
||||
self.assertEqual(res, None)
|
||||
mock_os_utils.create_user.assert_called_with('fake_user', 'fake_pass')
|
||||
mock_os_utils.set_user_info.assert_called_with(
|
||||
'fake_user', disabled=False, expire_interval=33155827200.0,
|
||||
full_name='fake user')
|
||||
mock_os_utils.add_user_to_local_group.assert_called_with(
|
||||
'fake_user', 'Users')
|
||||
mock_set_ssh_keys.assert_called_with('fake_user', ['test2', 'test1'],
|
||||
mock_os_utils)
|
||||
mock_user_logon.assert_called_with('fake_user', 'fake_pass',
|
||||
mock_os_utils)
|
||||
mock_get_pass.assert_called_with(fake_user, mock_os_utils)
|
||||
|
||||
@mock.patch(MODPATH + '.UsersPlugin._get_password')
|
||||
def test__create_user_inactive_with_create_home(self, mock_get_pass):
|
||||
fake_user = {
|
||||
'inactive': True,
|
||||
'expiredate': '3020-09-01',
|
||||
'no_create_home': False
|
||||
}
|
||||
mock_get_pass.return_value = 'fake_pass'
|
||||
mock_os_utils = mock.MagicMock()
|
||||
|
||||
with self.assertRaises(exception.CloudbaseInitException) as cm:
|
||||
self.users_plugin._create_user(fake_user, mock_os_utils)
|
||||
expected = ("The user is required to be enabled if public_keys "
|
||||
"or create_home are set")
|
||||
self.assertEqual(expected, str(cm.exception))
|
||||
|
||||
@mock.patch(MODPATH + '.UsersPlugin._create_user')
|
||||
@mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
|
||||
def test_process_user(self, mock_get_os_utils, mock_create_user):
|
||||
fake_data = [
|
||||
{
|
||||
'name': 'fake_user',
|
||||
'gecos': 'fake user name',
|
||||
'primary_group': 'Users',
|
||||
'groups': 'test',
|
||||
'ssh_authorized_keys': ["test2", "test1"],
|
||||
'inactive': False,
|
||||
'expiredate': '2020-09-01',
|
||||
'passwd': 'Passw0rd'
|
||||
}
|
||||
]
|
||||
with testutils.LogSnatcher(MODPATH) as snatcher:
|
||||
res = self.users_plugin.process(fake_data)
|
||||
self.assertEqual([], snatcher.output)
|
||||
self.assertEqual(res, False)
|
@ -154,6 +154,59 @@ The following cloud-config directives are supported:
|
||||
|
||||
set_hostname: newhostname
|
||||
|
||||
* groups - Create local groups and add existing users to those local groups.
|
||||
|
||||
The definition of the groups consists of a list in the format:
|
||||
|
||||
<group_name>: [<user1>, <user2>]
|
||||
|
||||
The list of users can be empty, when creating a group without members.
|
||||
|
||||
*Example:*
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
groups:
|
||||
- windows-group: [user1, user2]
|
||||
- cloud-users
|
||||
|
||||
* users - Create and configure local users.
|
||||
|
||||
The users are defined as a list. Each element from the list represents a user.
|
||||
Each user can have the the following attributes defined:
|
||||
|
||||
1. name - The username (required string).
|
||||
2. gecos - the user description.
|
||||
3. primary_group - the user's primary group.
|
||||
4. groups - the user's groups. On Windows, primary_group and groups are concatenated.
|
||||
5. passwd - the user's password. On Linux, the password is a hashed string,
|
||||
whereas on Windows the password is a plaintext string.
|
||||
If the password is not defined, a random password will be set.
|
||||
6. inactive - boolean value, defaults to False. If set to True, the user will
|
||||
be disabled.
|
||||
7. expiredate - a string in the format <year>-<month>-<day>. Example: 2020-10-01.
|
||||
8. ssh_authorized_keys - a list of SSH public keys, that will be set in
|
||||
~/.ssh/authorized_keys.
|
||||
|
||||
*Example:*
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
users:
|
||||
-
|
||||
name: Admin
|
||||
-
|
||||
name: brian
|
||||
gecos: 'Brian Cohen'
|
||||
primary_group: Users
|
||||
groups: cloud-users
|
||||
passwd: StrongPassw0rd
|
||||
inactive: False
|
||||
expiredate: 2020-10-01
|
||||
ssh_authorized_keys:
|
||||
- ssh-rsa AAAB...byV
|
||||
- ssh-rsa AAAB...ctV
|
||||
|
||||
|
||||
Multi-part content
|
||||
------------------
|
||||
|
Loading…
Reference in New Issue
Block a user