Commit 5e991d07 authored by Julien Muchembled's avatar Julien Muchembled

vm: change how commands can be easily run with a normal user account on the guest

9p has no option to map uid/gid, which means that the user on the guest should
have the same ids than on the host to avoid issues when accessing mount points.

This is easier to create such user in 'vm.run' rather than in 'vm.install-*'.
In other words, this commit reverts to the previous behaviour that
'vm.install-*' does not create a normal user account by default.
parents a6c0f02d f8c6faef
Pipeline #3145 skipped
......@@ -669,9 +669,17 @@ late-command
``preseed.preseed/late_command`` option.
packages
Extra packages to install. Defaults to ``ssh``.
Extra packages to install.
Like for `late-command`, do not use ``preseed.pkgsel/include``.
vm.run
Boolean value that is `true` by default, to configure the VM for use with
the `slapos.recipe.build:vm.run`_ recipe:
- make sure that the `ssh` and `sudo` packages are installed
- an SSH key is automatically created with ``ssh-keygen``, and it can be
used to connect as `root`
ISO sections
~~~~~~~~~~~~
......@@ -689,18 +697,10 @@ ISO sections
User setup
~~~~~~~~~~
This recipe comes with 2 specific rules:
- If the `ssh` package is installed, which is required for
`slapos.recipe.build:vm.run`_, an SSH key is automatically created with
``ssh-keygen``, and it can be used to connect as the normal user account,
or as `root` if ``passwd/make-user`` is ``false``.
By default, there's no normal user created. Another rule is that a random
password is automatically generated if there is no password specified.
- If the user account is locked and if ``sudo`` is installed, a sudo rule is
added to allow this account to become root without password.
By default, both SSH and sudo are installed, a `slapos` user account is created
and both root and user accounts are locked.
You have nothing to do if you only plan to use the VM with `vm.run`.
For more information about the ``passwd/*`` preseed values, you can look at
the ``user-setup-udeb`` package at
......@@ -732,7 +732,7 @@ Example
# minimal size
preseed.recommends = false
preseed.tasks =
packages = localepurge ssh
packages = localepurge
[debian-jessie]
x86_64.iso = debian-amd64-netinst.iso
......@@ -783,7 +783,15 @@ stop-ssh
Default: systemctl stop ssh
user
SSH connects with this user. Default: slapos
Execute commands with this user. The value can be ``root``. By default,
it is empty and it means that:
- a ``slapos`` user is created with the same uid/gid than the user using
this recipe on the host, which can help accessing mount points;
- sudo must be installed and the created user is allowed to become root
without password.
In any case, SSH connects as root.
wait-ssh
Time to wait for (re)boot. The recipe fails if it can't connect to the SSH
......
......@@ -29,11 +29,10 @@ import base64, os, select, shutil, socket
import subprocess, sys, tempfile, threading, time
from contextlib import contextmanager
from os.path import join
from slapos.recipe import EnvironMixin, logger, rmtree
from slapos.recipe import EnvironMixin, generatePassword, logger, rmtree
from zc.buildout import UserError
ARCH = os.uname()[4]
USER = 'slapos'
@contextmanager
def building_directory(directory):
......@@ -47,6 +46,7 @@ def building_directory(directory):
shutil.rmtree(directory)
raise
is_true = ('false', 'true').index
class Popen(subprocess.Popen):
......@@ -116,14 +116,13 @@ class InstallDebianRecipe(BaseRecipe):
finish-install/reboot_in_progress = note
preseed/url = file:///dev/null
passwd/make-user = true
passwd/root-login = false
clock-setup/ntp = false
time/zone = UTC
language = C
country = FR
keymap = us
passwd/make-user = false
passwd/root-login = true
partman-auto/method = regular
partman-auto/expert_recipe = : 1 1 -1 ext4 $primary{ } $bootable{ } method{ format } format{ } use_filesystem{ } filesystem{ ext4 } mountpoint{ / } options/discard{ } options/noatime{ } .
"""
......@@ -176,36 +175,40 @@ class InstallDebianRecipe(BaseRecipe):
continue
v = ''
cmdline[owner + k] = v
packages = options.get('packages', 'ssh').split()
vm_run = is_true(options.get('vm.run', 'true'))
packages = ['ssh', 'sudo'] if vm_run else []
packages += options.get('packages', '').split()
if packages:
cmdline['pkgsel/include'] = ','.join(packages)
if ('false', 'true').index(cmdline['passwd/make-user']):
user = cmdline.setdefault('passwd/username', USER)
generated = []
for x, p in (('root', 'passwd/root-login'),
('user', 'passwd/make-user')):
if is_true(cmdline[p]):
p = 'passwd/%s-password' % x
if x == 'user':
x = cmdline.get('passwd/username')
if not x:
raise UserError('passwd/username is empty')
cmdline.setdefault('passwd/user-fullname', '')
if not (cmdline.get('passwd/user-password') or
cmdline.get('passwd/user-password-crypted')):
cmdline['passwd/user-password-crypted'] = '!'
else:
user = None
if not (cmdline.get(p) or cmdline.get(p + '-crypted')):
cmdline[p] = cmdline[p + '-again'] = passwd = generatePassword()
generated.append((x, passwd))
env = self.environ
location = self.vm
with building_directory(location):
if 'ssh' in packages:
if vm_run:
key = self.ssh_key
subprocess.check_call(('ssh-keygen', '-N', '', '-f', key), env=env)
key += '.pub'
with open(key) as f:
os.remove(key)
key = f.read().strip()
cmd = "mkdir -m 0700 ~/.ssh && echo %s > ~/.ssh/authorized_keys" % key
self.late_command.append("su -c '%s' %s" % (cmd, user) if user else cmd)
if user and cmdline.get('passwd/user-password-crypted') == '!':
self.late_command.append(
"(cd /etc/sudoers.d && echo %s ALL=NOPASSWD: ALL >%s && chmod 440 %s)"
% (user, user, user))
"mkdir -m 0700 ~/.ssh && echo %s > ~/.ssh/authorized_keys" % key)
if self.late_command:
cmdline['preseed/late_command'] = (
'cd /target && echo %s|usr/bin/base64 -d|chroot . sh'
......@@ -239,17 +242,30 @@ class InstallDebianRecipe(BaseRecipe):
'DOS/MBR boot sector'):
raise Exception('non bootable image')
if generated:
fd = os.open(join(location, 'passwd'), open_flags, 0600)
try:
for generated in generated:
os.write(fd, "%s:%s\n" % generated)
finally:
os.close(fd)
return [location]
class RunRecipe(BaseRecipe):
init = """set -e
cd /mnt; set %s; mkdir -p $*; for tag; do
mount -t 9p -o trans=virtio,version=9p2000.L,noatime $tag $tag
done
"""
command = """set -e
[ "$USER" = root ] || SUDO=sudo
reboot() {
unset -f reboot
$SUDO %s
(while pgrep -x sshd; do sleep 1; done >/dev/null; exec $SUDO reboot
%s%s
(while pgrep -x sshd; do sleep 1; done >/dev/null; %sreboot
) >/dev/null 2>&1 &
exit
}
......@@ -258,9 +274,6 @@ map() {
echo /mnt/buildout$x
}
PARTDIR=`map %s`
$SUDO sh -c 'cd /mnt; set %s; mkdir -p $*; for tag; do
mount -t 9p -o trans=virtio,version=9p2000.L,noatime $tag $tag
done'
"""
def __init__(self, buildout, name, options):
......@@ -285,13 +298,31 @@ done'
% (i, path),
'-device', 'virtio-9p-pci,id=fs%s,fsdev=fsdev%s,mount_tag=%s'
% (i, i, tag))
init = self.command % (
options.get('stop-ssh', 'systemctl stop ssh'),
self.buildout['buildout']['directory'],
location, ' '.join(self.mount_dict))
init = self.init % ' '.join(self.mount_dict)
user = options.get('user')
if user == 'root':
init += 'cd; exec sh'
sudo = ''
else:
sudo = 'sudo '
if not user:
init += """user=slapos gid=%s
cd /etc/sudoers.d
[ -f $user ] || {
groupadd -g $gid $user || :
useradd -m -u %s -g $gid $user
echo $user ALL=NOPASSWD: ALL >$user
chmod 440 $user
}
""" % (os.getgid(), os.getuid())
user = '$user'
init += 'exec su -ls /bin/sh ' + user
header = self.command % (
sudo, options.get('stop-ssh', 'systemctl stop ssh'), sudo,
self.buildout['buildout']['directory'], location)
commands = map(options.__getitem__,
options.get('commands', 'command').split())
user = options.get('user', USER)
hostfwd_retries = 9
wait_ssh = int(options.get('wait-ssh') or 60)
with building_directory(location):
......@@ -312,21 +343,21 @@ done'
options['dist'], snapshot=True, ssh=ssh[1]) + [
'-vnc', 'unix:' + vnc] + mount_args
s.close()
p = Popen(args, stderr=subprocess.PIPE, env=dict(env,
qemu = Popen(args, stderr=subprocess.PIPE, env=dict(env,
TMPDIR=location, # for snapshot
))
try:
while not select.select((p.stderr,), (), (), 1)[0]:
while not select.select((qemu.stderr,), (), (), 1)[0]:
if os.path.exists(vnc):
break
else:
err = p.communicate()[1]
err = qemu.communicate()[1]
sys.stderr.write(err)
if ('could not set up host forwarding rule' in err and
hostfwd_retries):
hostfwd_retries -= 1
continue
raise subprocess.CalledProcessError(p.returncode, args)
raise subprocess.CalledProcessError(qemu.returncode, args)
for command in commands:
timeout = time.time() + wait_ssh
while 1:
......@@ -339,14 +370,20 @@ done'
raise Exception("Can not SSH to VM after %s seconds"
% wait_ssh)
time.sleep(1)
subprocess.check_call(('ssh', '-n', '-i', self.ssh_key,
args = ('ssh', '-i', self.ssh_key,
'-o', 'BatchMode=yes',
'-o', 'UserKnownHostsFile=' + os.devnull,
'-o', 'StrictHostKeyChecking=no',
'-p', str(ssh[1]), user + '@' + ssh[0], init + command),
env=env)
'-p', str(ssh[1]), 'root@' + ssh[0], init)
p = Popen(args, stdin=subprocess.PIPE, env=env)
try:
p.communicate(header + command)
if p.returncode:
raise subprocess.CalledProcessError(p.returncode, args)
finally:
p.stop()
finally:
qemu.stop()
break
finally:
shutil.rmtree(tmp)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment