← Back to team overview

ubuntu-bugcontrol team mailing list archive

[Merge] ~jslarraz/ubuntu-qa-tools:uvt-snap into ubuntu-qa-tools:master

 

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,

Follow ups