Merge "Implement cloud-config users and groups plugins"
This commit is contained in:
commit
ea51dfb053
|
@ -57,6 +57,10 @@ class BaseOSUtils(object):
|
||||||
def rename_user(self, username, new_username):
|
def rename_user(self, username, new_username):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def set_user_info(self, username, full_name=None,
|
||||||
|
disabled=False, expire_interval=None):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def enum_users(self):
|
def enum_users(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -69,6 +73,12 @@ class BaseOSUtils(object):
|
||||||
def add_user_to_local_group(self, username, groupname):
|
def add_user_to_local_group(self, username, groupname):
|
||||||
raise NotImplementedError()
|
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):
|
def set_host_name(self, new_host_name):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
|
@ -474,6 +474,35 @@ class WindowsUtils(base.BaseOSUtils):
|
||||||
raise exception.CloudbaseInitException(
|
raise exception.CloudbaseInitException(
|
||||||
"Renaming user failed: %s" % ex.args[2])
|
"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):
|
def enum_users(self):
|
||||||
usernames = []
|
usernames = []
|
||||||
resume_handle = 0
|
resume_handle = 0
|
||||||
|
@ -531,6 +560,34 @@ class WindowsUtils(base.BaseOSUtils):
|
||||||
raise exception.CloudbaseInitException(
|
raise exception.CloudbaseInitException(
|
||||||
"Setting password expiration failed: %s" % ex.args[2])
|
"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
|
@staticmethod
|
||||||
def _get_cch_referenced_domain_name(domain_name):
|
def _get_cch_referenced_domain_name(domain_name):
|
||||||
return wintypes.DWORD(
|
return wintypes.DWORD(
|
||||||
|
@ -562,11 +619,13 @@ class WindowsUtils(base.BaseOSUtils):
|
||||||
3, ctypes.pointer(lmi), 1)
|
3, ctypes.pointer(lmi), 1)
|
||||||
|
|
||||||
if ret_val == self.NERR_GroupNotFound:
|
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:
|
elif ret_val == self.ERROR_ACCESS_DENIED:
|
||||||
raise exception.CloudbaseInitException('Access denied')
|
raise exception.CloudbaseInitException('Access denied')
|
||||||
elif ret_val == self.ERROR_NO_SUCH_MEMBER:
|
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:
|
elif ret_val == self.ERROR_MEMBER_IN_ALIAS:
|
||||||
# The user is already a member of the group
|
# The user is already a member of the group
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -32,6 +32,10 @@ PLUGINS = collections.OrderedDict([
|
||||||
'cloudconfigplugins.set_hostname.SetHostnamePlugin'),
|
'cloudconfigplugins.set_hostname.SetHostnamePlugin'),
|
||||||
('ntp', 'cloudbaseinit.plugins.common.userdataplugins.'
|
('ntp', 'cloudbaseinit.plugins.common.userdataplugins.'
|
||||||
'cloudconfigplugins.set_ntp.SetNtpPlugin'),
|
'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.'
|
('runcmd', 'cloudbaseinit.plugins.common.userdataplugins.'
|
||||||
'cloudconfigplugins.runcmd.RunCmdPlugin'),
|
'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):
|
def test_user_does_not_exist(self):
|
||||||
self._test_user_exists(exists=False)
|
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):
|
def test_sanitize_shell_input(self):
|
||||||
unsanitised = ' " '
|
unsanitised = ' " '
|
||||||
response = self._winutils.sanitize_shell_input(unsanitised)
|
response = self._winutils.sanitize_shell_input(unsanitised)
|
||||||
|
@ -2589,6 +2628,63 @@ class TestWindowsUtils(testutils.CloudbaseInitTestBase):
|
||||||
*([mock.Mock()] * 2))
|
*([mock.Mock()] * 2))
|
||||||
self._test_enum_users(exc=exc)
|
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):
|
def test_enum_users(self):
|
||||||
self._test_enum_users(resume_handle=False)
|
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:
|
||||||
#cloud-config
|
#cloud-config
|
||||||
set_hostname: newhostname
|
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
|
||||||
|
|
||||||
|
|
||||||
* ntp - Set NTP servers. The definition is a dict with the following attributes:
|
* ntp - Set NTP servers. The definition is a dict with the following attributes:
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue