Jorge Sancho Larraz has proposed merging ~jslarraz/ubuntu-qa-tools:uvt-snap into ubuntu-qa-tools:master.
Commit message: Add support for uvt snap Requested reviews: Ubuntu Bug Control (ubuntu-bugcontrol) For more details, see: https://code.launchpad.net/~jslarraz/ubuntu-qa-tools/+git/ubuntu-qa-tools/+merge/462951 Given the limited set of changes needed to properly support uvt in a snap format I wonder if it will be possible to include those changes in the master branch to make it easier to maintain (in contrast of rebasing onto master for new commits) -- Your team Ubuntu Bug Control is requested to review the proposed merge of ~jslarraz/ubuntu-qa-tools:uvt-snap into ubuntu-qa-tools:master.
diff --git a/snap/hooks/install b/snap/hooks/install new file mode 100644 index 0000000..7b412fd --- /dev/null +++ b/snap/hooks/install @@ -0,0 +1,29 @@ +#!/bin/bash + +# Add libvirt configuration to nsswitch.conf +cat > /etc/nsswitch.conf << "EOF" +# /etc/nsswitch.conf +# +# Example configuration of GNU Name Service Switch functionality. +# If you have the `glibc-doc-reference' and `info' packages installed, try: +# `info libc "Name Service Switch"' for information about this file. + +passwd: files systemd sss +group: files systemd sss +shadow: files systemd sss +gshadow: files systemd + +hosts: files mdns4_minimal [NOTFOUND=return] libvirt dns myhostname +networks: files + +protocols: db files +services: db files sss +ethers: db files +rpc: db files + +netgroup: nis sss +automount: sss +EOF + +# Make /var/lib/libvirt/dnsmasq to show where expected by libnss_libvirt +ln -s /var/lib/snapd/hostfs/var/lib/libvirt/dnsmasq/ /var/lib/libvirt/ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..10284c2 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,104 @@ +name: uncomplicated-vm-tools +version: '0.1' +summary: "Uncomplicated VM Tools (uvt)" +description: | + uvt is essentially a wrapper script for virsh and virt-install for both + making VM creation repeatable and to help batch commands to multiple VMs. + These tools use kvm and libvirt, the preferred virtualization technology + in Ubuntu. You can check if kvm virtualization is supported by using the + `kvm-ok` command. +confinement: strict +grade: stable +base: core22 + +layout: + # Layouts are required by virt-install to work + /usr/lib/x86_64-linux-gnu/girepository-1.0: + bind: $SNAP/usr/lib/x86_64-linux-gnu/girepository-1.0 + /usr/share/misc: + bind: $SNAP/usr/share/misc + /var/lib/usbutils: + bind: $SNAP/var/lib/usbutils + + # Address resolution + /etc/nsswitch.conf: + bind-file: $SNAP_COMMON/etc/nsswitch.conf + /var/lib/libvirt: + bind: $SNAP_COMMON/var/lib/libvirt + +parts: + uvt: + plugin: python + source: vm-tools + stage-packages: + - cpu-checker # kvm-ok + - genisoimage + - xorriso + - whois + - cpio + - gzip + - qemu-utils # qemu-img + - virtinst # virt-install + - virt-viewer # virt-viewer + - libvirt-clients # virsh + - libnss-libvirt # network address resolution + - libyajl2 # network address resolution + - acl # setfacl: enable libvirt-qemu user to access required folders + - python3-lxml + - python3-distro-info + - gpg # export public gpg key + - openssh-client # ssh connections + override-build: | + snapcraftctl build + cp uvt $SNAPCRAFT_PRIME + cp uvt-completion.bash $SNAPCRAFT_PRIME + +apps: + uvt: + command: "bin/python3 $SNAP/uvt" + completer: "uvt-completion.bash" + plugs: + - home + - network + - libvirt # everything + - run-libvirt-libvirt-sock-ro # uvt view + - x11 # uvt view + - gpg-public-keys # uvt new / uvt repo (to add gpg key to vm) + - etc-default-keyboard # uvt new (to get keyboard configuration) + - ssh-keys # Don't autoconnect, only compat mode + - dot-uvt-dot-conf # Don't autoconnect, only compat mode + environment: + PYTHONPATH: "/usr/lib/python3/dist-packages:$SNAP/usr/lib/python3/dist-packages:$SNAP/lib/python3.10/site-packages:$SNAP/usr/share/virt-manager" + +plugs: + etc-default-keyboard: + interface: system-files + read: + - /etc/default/keyboard + + run-libvirt-libvirt-sock-ro: + interface: system-files + write: + - /run/libvirt/libvirt-sock-ro + + hostfs-var-lib-libvirt-dnsmasq: + interface: system-files + read: + - /var/lib/snapd/hostfs/var/lib/libvirt/dnsmasq + + + # Only for compat mode + dot-uvt-dot-conf: + interface: personal-files + read: + - $HOME/.uvt.conf + + dot-cache-virt-manager-virt-install-dot-log: + interface: personal-files + write: + - $HOME/.cache/virt-manager/virt-install.log + + dot-config-virt-viewer-settings: + interface: personal-files + read: + - $HOME/.config/virt-viewer/settings \ No newline at end of file diff --git a/vm-tools/uvt b/vm-tools/uvt index 0702939..068df2f 100755 --- a/vm-tools/uvt +++ b/vm-tools/uvt @@ -468,6 +468,58 @@ def cmd_cmd(): print("Error: VM '%s' command failed. Aborting." % machine, file=sys.stderr) sys.exit(1) +def cmd_ssh(): + '''Run a command inside a virtual machine''' + + usage = "usage: %prog ssh [options] <vm>" + + epilog = "\n" + \ + "Eg:\n" + \ + "$ uvt ssh sec-jammy-amd64\n\n" + \ + "This will open an interactive session on the single VM named 'sec-jammy-amd64'\n" + + optparse.OptionParser.format_epilog = lambda self, formatter: self.epilog + parser = optparse.OptionParser(usage = usage, epilog = epilog) + + parser.add_option("-s", "--start", dest="start", default=False, action='store_true', + help="Start the VM (and shutdown if it wasn't running") + + parser.add_option("-t", "--timeout", dest="timeout", default=90, metavar="TIMEOUT", + help="wait TIMEOUT seconds for VM to come up if -s is used (default: %default)") + + parser.add_option("-f", "--force-ssh", dest="force_ssh", default=False, action='store_true', + help="force the SSH keys to be taken") + + parser.add_option("-r", "--root", dest="root", default=False, action='store_true', + help="login to the VM as root") + + parser.add_option("-u", "--user", dest="user", default=None, metavar="USER", + help="login to the VM as user") + + parser.add_option("-q", "--quiet", dest="quiet", default=False, action='store_true', + help="only report hostnames and output") + + (opt, args) = parser.parse_args() + machine = args[0] + + if opt.user is not None and opt.root: + print("Error: may specify only one of --root and --user.\n", file=sys.stderr) + sys.exit(1) + + print("----- %s -----" % machine) + if check_vm_exists(machine) == False: + print("Error: VM '%s' does not exist, skipping." % machine, file=sys.stderr) + return + + result = vm_run_command(machine, "bash", root=opt.root, start=opt.start, + start_timeout=opt.timeout, force_keys=opt.force_ssh, + quiet=opt.quiet, output=True, interactive=True, + verbose=False, user=opt.user) + + if result == False: + print("Error: VM '%s' command failed. Aborting." % machine, file=sys.stderr) + sys.exit(1) + def cmd_repo(): '''Adds or removes a local repo to a VM''' @@ -1454,6 +1506,9 @@ def vm_run_command(vm_name, command, root=False, start=False, ssh_command += ['-q'] ssh_command += ['-o', 'BatchMode=yes'] ssh_command += ['-i', uvt_conf['vm_ssh_key'].split(".pub")[0]] + if is_snap(): + ssh_command += ['-o', 'StrictHostKeyChecking=accept-new'] + ssh_command += ['-o', 'UserKnownHostsFile=' + os.path.expanduser("~/.ssh/known_hosts")] ssh_command += [dns_name, command] @@ -1479,7 +1534,10 @@ def vm_run_command(vm_name, command, root=False, start=False, def remove_ssh_keys(vm_name): '''Removes SSH keys for a host''' - runcmd(["ssh-keygen", "-R", vm_name]) + cmd = ["ssh-keygen", "-R", vm_name] + if is_snap(): + cmd += ["-f", os.path.expanduser("~/.ssh/known_hosts")] + rc, out = runcmd(cmd) def crypt_password(password): '''Crypts a password using mkpasswd''' @@ -1500,7 +1558,6 @@ def vm_start(vm_name): rc, out = runcmd(["virsh", "--connect", uvt_conf["vm_connect"], "start", vm_name]) - def vm_destroy(vm_name): '''Powers off a VM''' rc, out = runcmd(["virsh", "--connect", uvt_conf["vm_connect"], @@ -1578,7 +1635,9 @@ def vm_start_wait(vm_name, timeout=1800, quiet=False, clone_name=None): def vm_ping(vm_name): '''Attempts to ping a VM''' - rc, out = runcmd(["ping", "-c1", "-w1", vm_name]) + rc = 0 + if not is_snap(): + rc, out = runcmd(["ping", "-c1", "-w1", vm_name]) if rc == 0 and ssh_connect(vm_name) == True: return vm_name return "" @@ -3093,6 +3152,9 @@ def runcmd(command, input = None, stderr = subprocess.STDOUT, '''Try to execute given command (array) and return its stdout, or return a textual error if it failed.''' + if is_snap(): + stderr = subprocess.DEVNULL + try: sp = subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=True, shell=shell) except OSError as e: @@ -3127,9 +3189,18 @@ def get_gpg_public_key(keyid=None): return None print("Exporting GPG key '%s'" % keyid) - rc, out = runcmd(['gpg', '--export', '--armor', keyid]) + cmd = ['gpg', '--export', '--armor'] + if is_snap(): + cmd += ['--homedir', os.getenv('SNAP_REAL_HOME', os.path.expanduser("~")) + '/.gnupg'] + rc, out = runcmd(cmd + [keyid]) # make sure something actually got exported - if rc != 0 or not out.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----'): + # FIXME: without the ability to create lock files, gpg --export returns an error code even if it also return the public key, + # splitting this check is a workaround that should be remove after https://github.com/snapcore/snapd/pull/13540 being released + # with next snapd version (2.62) + if rc != 0 and not is_snap(): + print("Error: Failed to export public key '%s'." % keyid, file=sys.stderr) + return None + if not out.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----'): print("Error: Failed to export public key '%s'." % keyid, file=sys.stderr) return None @@ -3341,9 +3412,11 @@ def check_required_tools(): 'kvm-ok' : 'cpu-checker', 'gzip' : 'gzip', 'cpio' : 'cpio', - 'kvm' : 'qemu-kvm', 'mkpasswd' : 'whois' } + if not is_snap(): + tools['kvm'] = 'qemu-kvm' + missing_tools = [] for tool in tools: ret, out = runcmd(['which', tool]) @@ -3487,15 +3560,15 @@ def load_uvt_config(): if not 'vm_xkboptions' in config: config['vm_xkboptions'] = keyboard.get('XKBOPTIONS', "") - # Set a default image size to 8GB and memory to 1024MB + # Set a default image size to and memory to default values if config.get('vm_image_size', "") == "": - config['vm_image_size'] = "8" + config['vm_image_size'] = "8" if not is_snap() else "20" if config.get('vm_memory', "") == "": - config['vm_memory'] = "1024" + config['vm_memory'] = "1024" if not is_snap() else "4096" - # Set default vcpus to 1 + # Set default vcpus if config.get('vm_vcpus', "") == "": - config['vm_vcpus'] = "1" + config['vm_vcpus'] = "1" if not is_snap() else "4" # Set a default username and password if config.get('vm_username', "") == "": @@ -3615,6 +3688,7 @@ repo Adds or removes a local repo to a VM update Runs a dist-upgrade inside a VM clone Clones a VM into a new one cmd Run a command inside a virtual machine +ssh Opens an interactive session with the VM list List virtual machines view Connect to a virtual machine with VNC config Create an optional config file (~/%s) @@ -3647,10 +3721,21 @@ class BetterUbuntuDistroInfo(distro_info.UbuntuDistroInfo): return release.split()[0] # handle '16.04 LTS' vs '17.10' +def is_snap(): + if os.getenv("SNAP") is not None: + return True + return False + # # Main program # +# Update home if needed +if (os.getenv("UVT_SNAP_COMPAT_MODE") is not None) and (os.getenv("SNAP_REAL_HOME") is not None): + os.environ["HOME"] = os.getenv("SNAP_REAL_HOME") +elif os.getenv("SNAP_USER_COMMON") is not None: + os.environ["HOME"] = os.getenv("SNAP_USER_COMMON") + config_file = ".uvt.conf" cmd = None @@ -3683,6 +3768,7 @@ commands = { 'update' : cmd_update, 'clone' : cmd_clone, 'cmd' : cmd_cmd, + 'ssh' : cmd_ssh, 'list' : cmd_list, 'config' : cmd_config, 'dump' : cmd_dump,
_______________________________________________ Mailing list: https://launchpad.net/~ubuntu-bugcontrol Post to : ubuntu-bugcontrol@lists.launchpad.net Unsubscribe : https://launchpad.net/~ubuntu-bugcontrol More help : https://help.launchpad.net/ListHelp