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
...@@ -669,9 +669,17 @@ late-command ...@@ -669,9 +669,17 @@ late-command
``preseed.preseed/late_command`` option. ``preseed.preseed/late_command`` option.
packages packages
Extra packages to install. Defaults to ``ssh``. Extra packages to install.
Like for `late-command`, do not use ``preseed.pkgsel/include``. 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 ISO sections
~~~~~~~~~~~~ ~~~~~~~~~~~~
...@@ -689,18 +697,10 @@ ISO sections ...@@ -689,18 +697,10 @@ ISO sections
User setup User setup
~~~~~~~~~~ ~~~~~~~~~~
This recipe comes with 2 specific rules: 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 `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``.
- If the user account is locked and if ``sudo`` is installed, a sudo rule is You have nothing to do if you only plan to use the VM with `vm.run`.
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.
For more information about the ``passwd/*`` preseed values, you can look at For more information about the ``passwd/*`` preseed values, you can look at
the ``user-setup-udeb`` package at the ``user-setup-udeb`` package at
...@@ -732,7 +732,7 @@ Example ...@@ -732,7 +732,7 @@ Example
# minimal size # minimal size
preseed.recommends = false preseed.recommends = false
preseed.tasks = preseed.tasks =
packages = localepurge ssh packages = localepurge
[debian-jessie] [debian-jessie]
x86_64.iso = debian-amd64-netinst.iso x86_64.iso = debian-amd64-netinst.iso
...@@ -783,7 +783,15 @@ stop-ssh ...@@ -783,7 +783,15 @@ stop-ssh
Default: systemctl stop ssh Default: systemctl stop ssh
user 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 wait-ssh
Time to wait for (re)boot. The recipe fails if it can't connect to the 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 ...@@ -29,11 +29,10 @@ import base64, os, select, shutil, socket
import subprocess, sys, tempfile, threading, time import subprocess, sys, tempfile, threading, time
from contextlib import contextmanager from contextlib import contextmanager
from os.path import join 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 from zc.buildout import UserError
ARCH = os.uname()[4] ARCH = os.uname()[4]
USER = 'slapos'
@contextmanager @contextmanager
def building_directory(directory): def building_directory(directory):
...@@ -47,6 +46,7 @@ def building_directory(directory): ...@@ -47,6 +46,7 @@ def building_directory(directory):
shutil.rmtree(directory) shutil.rmtree(directory)
raise raise
is_true = ('false', 'true').index
class Popen(subprocess.Popen): class Popen(subprocess.Popen):
...@@ -116,14 +116,13 @@ class InstallDebianRecipe(BaseRecipe): ...@@ -116,14 +116,13 @@ class InstallDebianRecipe(BaseRecipe):
finish-install/reboot_in_progress = note finish-install/reboot_in_progress = note
preseed/url = file:///dev/null preseed/url = file:///dev/null
passwd/make-user = true
passwd/root-login = false
clock-setup/ntp = false clock-setup/ntp = false
time/zone = UTC time/zone = UTC
language = C language = C
country = FR country = FR
keymap = us keymap = us
passwd/make-user = false
passwd/root-login = true
partman-auto/method = regular 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{ } . 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): ...@@ -176,36 +175,40 @@ class InstallDebianRecipe(BaseRecipe):
continue continue
v = '' v = ''
cmdline[owner + k] = 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: if packages:
cmdline['pkgsel/include'] = ','.join(packages) cmdline['pkgsel/include'] = ','.join(packages)
if ('false', 'true').index(cmdline['passwd/make-user']): generated = []
user = cmdline.setdefault('passwd/username', USER) 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', '') cmdline.setdefault('passwd/user-fullname', '')
if not (cmdline.get('passwd/user-password') or if not (cmdline.get(p) or cmdline.get(p + '-crypted')):
cmdline.get('passwd/user-password-crypted')): cmdline[p] = cmdline[p + '-again'] = passwd = generatePassword()
cmdline['passwd/user-password-crypted'] = '!' generated.append((x, passwd))
else:
user = None
env = self.environ env = self.environ
location = self.vm location = self.vm
with building_directory(location): with building_directory(location):
if 'ssh' in packages: if vm_run:
key = self.ssh_key key = self.ssh_key
subprocess.check_call(('ssh-keygen', '-N', '', '-f', key), env=env) subprocess.check_call(('ssh-keygen', '-N', '', '-f', key), env=env)
key += '.pub' key += '.pub'
with open(key) as f: with open(key) as f:
os.remove(key) os.remove(key)
key = f.read().strip() 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( self.late_command.append(
"(cd /etc/sudoers.d && echo %s ALL=NOPASSWD: ALL >%s && chmod 440 %s)" "mkdir -m 0700 ~/.ssh && echo %s > ~/.ssh/authorized_keys" % key)
% (user, user, user))
if self.late_command: if self.late_command:
cmdline['preseed/late_command'] = ( cmdline['preseed/late_command'] = (
'cd /target && echo %s|usr/bin/base64 -d|chroot . sh' 'cd /target && echo %s|usr/bin/base64 -d|chroot . sh'
...@@ -239,17 +242,30 @@ class InstallDebianRecipe(BaseRecipe): ...@@ -239,17 +242,30 @@ class InstallDebianRecipe(BaseRecipe):
'DOS/MBR boot sector'): 'DOS/MBR boot sector'):
raise Exception('non bootable image') 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] return [location]
class RunRecipe(BaseRecipe): 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 command = """set -e
[ "$USER" = root ] || SUDO=sudo
reboot() { reboot() {
unset -f reboot unset -f reboot
$SUDO %s %s%s
(while pgrep -x sshd; do sleep 1; done >/dev/null; exec $SUDO reboot (while pgrep -x sshd; do sleep 1; done >/dev/null; %sreboot
) >/dev/null 2>&1 & ) >/dev/null 2>&1 &
exit exit
} }
...@@ -258,9 +274,6 @@ map() { ...@@ -258,9 +274,6 @@ map() {
echo /mnt/buildout$x echo /mnt/buildout$x
} }
PARTDIR=`map %s` 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): def __init__(self, buildout, name, options):
...@@ -285,13 +298,31 @@ done' ...@@ -285,13 +298,31 @@ done'
% (i, path), % (i, path),
'-device', 'virtio-9p-pci,id=fs%s,fsdev=fsdev%s,mount_tag=%s' '-device', 'virtio-9p-pci,id=fs%s,fsdev=fsdev%s,mount_tag=%s'
% (i, i, tag)) % (i, i, tag))
init = self.command % ( init = self.init % ' '.join(self.mount_dict)
options.get('stop-ssh', 'systemctl stop ssh'), user = options.get('user')
self.buildout['buildout']['directory'], if user == 'root':
location, ' '.join(self.mount_dict)) 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__, commands = map(options.__getitem__,
options.get('commands', 'command').split()) options.get('commands', 'command').split())
user = options.get('user', USER)
hostfwd_retries = 9 hostfwd_retries = 9
wait_ssh = int(options.get('wait-ssh') or 60) wait_ssh = int(options.get('wait-ssh') or 60)
with building_directory(location): with building_directory(location):
...@@ -312,21 +343,21 @@ done' ...@@ -312,21 +343,21 @@ done'
options['dist'], snapshot=True, ssh=ssh[1]) + [ options['dist'], snapshot=True, ssh=ssh[1]) + [
'-vnc', 'unix:' + vnc] + mount_args '-vnc', 'unix:' + vnc] + mount_args
s.close() s.close()
p = Popen(args, stderr=subprocess.PIPE, env=dict(env, qemu = Popen(args, stderr=subprocess.PIPE, env=dict(env,
TMPDIR=location, # for snapshot TMPDIR=location, # for snapshot
)) ))
try: try:
while not select.select((p.stderr,), (), (), 1)[0]: while not select.select((qemu.stderr,), (), (), 1)[0]:
if os.path.exists(vnc): if os.path.exists(vnc):
break break
else: else:
err = p.communicate()[1] err = qemu.communicate()[1]
sys.stderr.write(err) sys.stderr.write(err)
if ('could not set up host forwarding rule' in err and if ('could not set up host forwarding rule' in err and
hostfwd_retries): hostfwd_retries):
hostfwd_retries -= 1 hostfwd_retries -= 1
continue continue
raise subprocess.CalledProcessError(p.returncode, args) raise subprocess.CalledProcessError(qemu.returncode, args)
for command in commands: for command in commands:
timeout = time.time() + wait_ssh timeout = time.time() + wait_ssh
while 1: while 1:
...@@ -339,14 +370,20 @@ done' ...@@ -339,14 +370,20 @@ done'
raise Exception("Can not SSH to VM after %s seconds" raise Exception("Can not SSH to VM after %s seconds"
% wait_ssh) % wait_ssh)
time.sleep(1) time.sleep(1)
subprocess.check_call(('ssh', '-n', '-i', self.ssh_key, args = ('ssh', '-i', self.ssh_key,
'-o', 'BatchMode=yes', '-o', 'BatchMode=yes',
'-o', 'UserKnownHostsFile=' + os.devnull, '-o', 'UserKnownHostsFile=' + os.devnull,
'-o', 'StrictHostKeyChecking=no', '-o', 'StrictHostKeyChecking=no',
'-p', str(ssh[1]), user + '@' + ssh[0], init + command), '-p', str(ssh[1]), 'root@' + ssh[0], init)
env=env) p = Popen(args, stdin=subprocess.PIPE, env=env)
try:
p.communicate(header + command)
if p.returncode:
raise subprocess.CalledProcessError(p.returncode, args)
finally: finally:
p.stop() p.stop()
finally:
qemu.stop()
break break
finally: finally:
shutil.rmtree(tmp) 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