diff --git a/bareon/drivers/data/nailgun.py b/bareon/drivers/data/nailgun.py index 0599ddd..4603122 100644 --- a/bareon/drivers/data/nailgun.py +++ b/bareon/drivers/data/nailgun.py @@ -42,6 +42,7 @@ LOG = logging.getLogger(__name__) CONF = cfg.CONF CONF.import_opt('prepare_configdrive', 'bareon.drivers.deploy.nailgun') CONF.import_opt('config_drive_path', 'bareon.drivers.deploy.nailgun') +CONF.import_opt('default_root_password', 'bareon.drivers.deploy.nailgun') def match_device(hu_disk, ks_disk): @@ -827,6 +828,21 @@ class NailgunBuildImage(BaseDataDriver, os = objects.Ubuntu(repos=repos, packages=packages, major=14, minor=4, proxies=proxies) + + # add root account + root_password = self.data.get('root_password') + hashed_root_password = self.data.get('hashed_root_password') + + # for backward compatibily set default password is no password provided + if root_password is None and hashed_root_password is None: + root_password = CONF.default_root_password + + os.add_user_account( + name='root', + password=root_password, + homedir='/root', + hashed_password=hashed_root_password, + ) return os def parse_schemes(self): diff --git a/bareon/drivers/deploy/nailgun.py b/bareon/drivers/deploy/nailgun.py index 6b54b80..6f043e5 100644 --- a/bareon/drivers/deploy/nailgun.py +++ b/bareon/drivers/deploy/nailgun.py @@ -108,6 +108,11 @@ opts = [ default=True, help='Add udev rules for NIC remapping' ), + cfg.StrOpt( + 'default_root_password', + default='r00tme', + help='Default password for root user', + ) ] cli_opts = [ @@ -492,7 +497,10 @@ class Manager(BaseDeployDriver): 'etc/nailgun-agent/config.yaml')) bu.append_lvm_devices_filter(chroot, CONF.multipath_lvm_filter, CONF.lvm_conf_path) + + root = driver_os.get_user_by_name('root') bu.do_post_inst(chroot, + hashed_root_password=root.hashed_password, allow_unsigned_file=CONF.allow_unsigned_file, force_ipv4_file=CONF.force_ipv4_file) # restore disabled hosts/resolv files @@ -594,7 +602,9 @@ class Manager(BaseDeployDriver): attempts=CONF.fetch_packages_attempts) LOG.debug('Post-install OS configuration') + root = driver_os.get_user_by_name('root') bu.do_post_inst(chroot, + hashed_root_password=root.hashed_password, allow_unsigned_file=CONF.allow_unsigned_file, force_ipv4_file=CONF.force_ipv4_file) diff --git a/bareon/objects/operating_system.py b/bareon/objects/operating_system.py index c8fe163..38ee92c 100644 --- a/bareon/objects/operating_system.py +++ b/bareon/objects/operating_system.py @@ -28,6 +28,15 @@ class OperatingSystem(object): def add_user_account(self, **kwargs): self.user_accounts.append(users.User(**kwargs)) + def get_user_by_name(self, name): + """Get User object by name from user_accounts. + + If there is no users with such name return None + """ + for user in self.user_accounts: + if user.name == name: + return user + def to_dict(self): return {'major': self.major, 'minor': self.minor, diff --git a/bareon/objects/users.py b/bareon/objects/users.py index cafa6c9..42ca0ab 100644 --- a/bareon/objects/users.py +++ b/bareon/objects/users.py @@ -12,13 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. +import crypt + +from bareon.utils import utils + class User(object): def __init__(self, name, password, homedir, sudo=None, ssh_keys=None, - shell="/bin/bash"): + shell="/bin/bash", hashed_password=None): self.name = name self.password = password self.homedir = homedir self.sudo = sudo or [] self.ssh_keys = ssh_keys or [] self.shell = shell + self._hashed_password = hashed_password + + @property + def hashed_password(self): + if self.password is None: + return self._hashed_password + + if self._hashed_password is None: + self._hashed_password = crypt.crypt(self.password, utils.gensalt()) + + return self._hashed_password diff --git a/bareon/tests/test_build_utils.py b/bareon/tests/test_build_utils.py index 74d24b0..d664401 100644 --- a/bareon/tests/test_build_utils.py +++ b/bareon/tests/test_build_utils.py @@ -181,14 +181,22 @@ class BuildUtilsTestCase(unittest2.TestCase): mock_open): mock_path.join.return_value = 'fake_path' mock_path.exists.return_value = True - bu.do_post_inst('chroot', allow_unsigned_file='fake_unsigned', + + # crypt.crypt('qwerty') + password = ('$6$KyOsgFgf9cLbGNST$Ej0Usihfy7W/WT2H0z0mC1DapC/IUpA0jF' + '.Fs83mFIdkGYHL9IOYykRCjfssH.YL4lHbmrvOd/6TIfiyh1hDY1') + + bu.do_post_inst('chroot', + hashed_root_password=password, + allow_unsigned_file='fake_unsigned', force_ipv4_file='fake_force_ipv4') file_handle_mock = mock_open.return_value.__enter__.return_value file_handle_mock.write.assert_called_once_with('manual\n') mock_exec_expected_calls = [ - mock.call('sed', '-i', 's%root:[\*,\!]%root:$6$IInX3Cqo$5xytL1VZb' - 'ZTusOewFnG6couuF0Ia61yS3rbC6P5YbZP2TYclwHqMq9e3Tg8rvQx' - 'hxSlBXP1DZhdUamxdOBXK0.%', 'fake_path'), + mock.call('sed', + '-i', + 's%root:[\*,\!]%root:{}%'.format(password), + 'fake_path'), mock.call('chroot', 'chroot', 'update-rc.d', 'puppet', 'disable'), mock.call('chroot', 'chroot', 'dpkg-divert', '--local', '--add', 'fake_path'), diff --git a/bareon/tests/test_manager.py b/bareon/tests/test_manager.py index f33fb39..d4ffbad 100644 --- a/bareon/tests/test_manager.py +++ b/bareon/tests/test_manager.py @@ -40,6 +40,12 @@ class TestImageBuild(unittest2.TestCase): def setUp(self, mock_driver, mock_http, mock_yaml): super(self.__class__, self).setUp() mock_driver.return_value = nailgun_data.NailgunBuildImage + + # TEST_ROOT_PASSWORD = crypt.crypt('qwerty') + self.TEST_ROOT_PASSWORD = ('$6$KyOsgFgf9cLbGNST$Ej0Usihfy7W/WT2H0z0mC' + '1DapC/IUpA0jF.Fs83mFIdkGYHL9IOYykRCjfssH.' + 'YL4lHbmrvOd/6TIfiyh1hDY1') + image_conf = { "image_data": { "/": { @@ -59,7 +65,8 @@ class TestImageBuild(unittest2.TestCase): 'priority': 1001 } ], - "codename": "trusty" + "codename": "trusty", + "hashed_root_password": self.TEST_ROOT_PASSWORD, } self.mgr = nailgun_deploy.Manager( nailgun_data.NailgunBuildImage(image_conf)) @@ -99,7 +106,10 @@ class TestImageBuild(unittest2.TestCase): 'trusty', 'fakesection', priority=None), objects.DEBRepo('mos', 'http://fakemos', 'mosX.Y', 'fakesection', priority=1000)], - packages=['fakepackage1', 'fakepackage2']) + packages=['fakepackage1', 'fakepackage2'], + user_accounts=[ + objects.User(name='root', password=None, homedir='/root', + hashed_password=self.TEST_ROOT_PASSWORD)]) self.mgr.driver.operating_system.proxies = objects.RepoProxies( proxies={'fake': 'fake'}, direct_repo_addr_list='fake_addr') @@ -187,7 +197,9 @@ class TestImageBuild(unittest2.TestCase): '/tmp/imgdir', packages=['fakepackage1', 'fakepackage2'], attempts=CONF.fetch_packages_attempts) mock_bu.do_post_inst.assert_called_once_with( - '/tmp/imgdir', allow_unsigned_file=CONF.allow_unsigned_file, + '/tmp/imgdir', + hashed_root_password=self.TEST_ROOT_PASSWORD, + allow_unsigned_file=CONF.allow_unsigned_file, force_ipv4_file=CONF.force_ipv4_file) signal_calls = mock_bu.stop_chrooted_processes.call_args_list diff --git a/bareon/utils/build.py b/bareon/utils/build.py index 0b4086e..2f08347 100644 --- a/bareon/utils/build.py +++ b/bareon/utils/build.py @@ -56,10 +56,6 @@ PROXY_PROTOCOLS = { ADDITIONAL_DEBOOTSTRAP_PACKAGES = ['ca-certificates', 'apt-transport-https'] -# NOTE(agordeev): hardcoded to r00tme -ROOT_PASSWORD = '$6$IInX3Cqo$5xytL1VZbZTusOewFnG6couuF0Ia61yS3rbC6P5YbZP2TYcl'\ - 'wHqMq9e3Tg8rvQxhxSlBXP1DZhdUamxdOBXK0.' - def run_debootstrap(uri, suite, chroot, arch='amd64', eatmydata=False, attempts=10, proxies=None, direct_repo_addr=None): @@ -180,11 +176,12 @@ def clean_apt_settings(chroot, allow_unsigned_file='allow_unsigned_packages', clean_dirs(chroot, dirs) -def do_post_inst(chroot, allow_unsigned_file='allow_unsigned_packages', +def do_post_inst(chroot, hashed_root_password, + allow_unsigned_file='allow_unsigned_packages', force_ipv4_file='force_ipv4'): # NOTE(agordeev): set up password for root utils.execute('sed', '-i', - 's%root:[\*,\!]%root:' + ROOT_PASSWORD + '%', + 's%root:[\*,\!]%root:' + hashed_root_password + '%', os.path.join(chroot, 'etc/shadow')) # NOTE(agordeev): backport from bash-script: # in order to prevent the later puppet workflow outage, puppet service diff --git a/bareon/utils/utils.py b/bareon/utils/utils.py index 3c7203c..c9a7eba 100644 --- a/bareon/utils/utils.py +++ b/bareon/utils/utils.py @@ -18,9 +18,11 @@ import json import locale import math import os +import random as _random import re import shlex import socket +import string import subprocess import time @@ -36,6 +38,7 @@ import urllib3 from bareon import errors from bareon.openstack.common import log as logging +random = _random.SystemRandom() LOG = logging.getLogger(__name__) @@ -419,6 +422,14 @@ def udevadm_settle(): execute('udevadm', 'settle', check_exit_code=[0]) +def gensalt(): + """Generate SHA-512 salt for crypt.crypt function.""" + letters = string.ascii_letters + string.digits + './' + sha512prefix = "$6$" + random_letters = ''.join(random.choice(letters) for _ in range(16)) + return sha512prefix + random_letters + + def dict_diff(dict1, dict2, sfrom="from", sto="to"): j1 = json.dumps(dict1, indent=2) j2 = json.dumps(dict2, indent=2) diff --git a/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/commands/build.py b/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/commands/build.py index 2320b2b..81b7dd5 100644 --- a/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/commands/build.py +++ b/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/commands/build.py @@ -189,6 +189,13 @@ class BuildCommand(command.Command): " files", action='append' ) + parser.add_argument( + '--root-password', + type=str, + help=("Root password for bootstrap image. PasswordAuthentication" + " by ssh still rejected by default! This password actual" + " only for tty login!"), + ) return parser def take_action(self, parsed_args): diff --git a/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/settings.yaml.sample b/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/settings.yaml.sample index 186b4e4..ee720fb 100644 --- a/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/settings.yaml.sample +++ b/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/settings.yaml.sample @@ -116,3 +116,5 @@ active_bootstrap_symlink: "/var/www/nailgun/bootstraps/active_bootstrap" # "user": "admin" # "password": "admin" +# User can provide default hashed root password for bootstrap image +# hashed_root_password: "$6$IInX3Cqo$5xytL1VZbZTusOewFnG6couuF0Ia61yS3rbC6P5YbZP2TYclwHqMq9e3Tg8rvQxhxSlBXP1DZhdUamxdOBXK0." diff --git a/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/utils/data.py b/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/utils/data.py index 774e1a9..879edd8 100644 --- a/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/utils/data.py +++ b/contrib/fuel_bootstrap/fuel_bootstrap_cli/fuel_bootstrap/utils/data.py @@ -72,6 +72,12 @@ class BootstrapDataBuilder(object): self.certs = data.get('certs') + self.root_password = data.get('root_password') + self.hashed_root_password = None + + if self.root_password is None: + self.hashed_root_password = CONF.hashed_root_password + def build(self): return { 'bootstrap': { @@ -93,7 +99,9 @@ class BootstrapDataBuilder(object): 'codename': self.ubuntu_release, 'output': self.output, 'packages': self._get_packages(), - 'image_data': self._prepare_image_data() + 'image_data': self._prepare_image_data(), + 'hashed_root_password': self.hashed_root_password, + 'root_password': self.root_password, } def _get_extra_dirs(self): diff --git a/etc/bareon/bareon.conf.sample b/etc/bareon/bareon.conf.sample index 944803c..2b7e0c9 100644 --- a/etc/bareon/bareon.conf.sample +++ b/etc/bareon/bareon.conf.sample @@ -209,3 +209,6 @@ log_file=/var/log/bareon.log #execute_retry_delay=2.0 +# Default password for root user +# (string value) +default_root_password=r00tme