| |
@@ -0,0 +1,291 @@
|
| |
+ #!/usr/bin/env python
|
| |
+
|
| |
+ import argparse
|
| |
+ import errno
|
| |
+ import json
|
| |
+ import os
|
| |
+ import shutil
|
| |
+ import shlex
|
| |
+ import signal
|
| |
+ import socket
|
| |
+ import subprocess
|
| |
+ import sys
|
| |
+ import tempfile
|
| |
+ import time
|
| |
+ import distutils.util
|
| |
+
|
| |
+ IDENTITY = """
|
| |
+ -----BEGIN RSA PRIVATE KEY-----
|
| |
+ MIIEpQIBAAKCAQEA1DrTSXQRF8isQQfPfK3U+eFC4zBrjur+Iy15kbHUYUeSHf5S
|
| |
+ jXPYbHYqD1lHj4GJajC9okle9rykKFYZMmJKXLI6987wZ8vfucXo9/kwS6BDAJto
|
| |
+ ZpZSj5sWCQ1PI0Ce8CbkazlTp5NIkjRfhXGP8mkNKMEhdNjaYceO49ilnNCIxhpb
|
| |
+ eH5dH5hybmQQNmnzf+CGCCLBFmc4g3sFbWhI1ldyJzES5ZX3ahjJZYRUfnndoUM/
|
| |
+ TzdkHGqZhL1EeFAsv5iV65HuYbchch4vBAn8jDMmHh8G1ixUCL3uAlosfarZLLyo
|
| |
+ 3HrZ8U/llq7rXa93PXHyI/3NL/2YP3OMxE8baQIDAQABAoIBAQCxuOUwkKqzsQ9W
|
| |
+ kdTWArfj3RhnKigYEX9qM+2m7TT9lbKtvUiiPc2R3k4QdmIvsXlCXLigyzJkCsqp
|
| |
+ IJiPEbJV98bbuAan1Rlv92TFK36fBgC15G5D4kQXD/ce828/BSFT2C3WALamEPdn
|
| |
+ v8Xx+Ixjokcrxrdeoy4VTcjB0q21J4C2wKP1wEPeMJnuTcySiWQBdAECCbeZ4Vsj
|
| |
+ cmRdcvL6z8fedRPtDW7oec+IPkYoyXPktVt8WsQPYkwEVN4hZVBneJPCcuhikYkp
|
| |
+ T3WGmPV0MxhUvCZ6hSG8D2mscZXRq3itXVlKJsUWfIHaAIgGomWrPuqC23rOYCdT
|
| |
+ 5oSZmTvFAoGBAPs1FbbxDDd1fx1hisfXHFasV/sycT6ggP/eUXpBYCqVdxPQvqcA
|
| |
+ ktplm5j04dnaQJdHZ8TPlwtL+xlWhmhFhlCFPtVpU1HzIBkp6DkSmmu0gvA/i07Z
|
| |
+ pzo5Z+HRZFzruTQx6NjDtvWwiXVLwmZn2oiLeM9xSqPu55OpITifEWNjAoGBANhH
|
| |
+ XwV6IvnbUWojs7uiSGsXuJOdB1YCJ+UF6xu8CqdbimaVakemVO02+cgbE6jzpUpo
|
| |
+ krbDKOle4fIbUYHPeyB0NMidpDxTAPCGmiJz7BCS1fCxkzRgC+TICjmk5zpaD2md
|
| |
+ HCrtzIeHNVpTE26BAjOIbo4QqOHBXk/WPen1iC3DAoGBALsD3DSj46puCMJA2ebI
|
| |
+ 2EoWaDGUbgZny2GxiwrvHL7XIx1XbHg7zxhUSLBorrNW7nsxJ6m3ugUo/bjxV4LN
|
| |
+ L59Gc27ByMvbqmvRbRcAKIJCkrB1Pirnkr2f+xx8nLEotGqNNYIawlzKnqr6SbGf
|
| |
+ Y2wAGWKmPyEoPLMLWLYkhfdtAoGANsFa/Tf+wuMTqZuAVXCwhOxsfnKy+MNy9jiZ
|
| |
+ XVwuFlDGqVIKpjkmJyhT9KVmRM/qePwgqMSgBvVOnszrxcGRmpXRBzlh6yPYiQyK
|
| |
+ 2U4f5dJG97j9W7U1TaaXcCCfqdZDMKnmB7hMn8NLbqK5uLBQrltMIgt1tjIOfofv
|
| |
+ BNx0raECgYEApAvjwDJ75otKz/mvL3rUf/SNpieODBOLHFQqJmF+4hrSOniHC5jf
|
| |
+ f5GS5IuYtBQ1gudBYlSs9fX6T39d2avPsZjfvvSbULXi3OlzWD8sbTtvQPuCaZGI
|
| |
+ Df9PUWMYZ3HRwwdsYovSOkT53fG6guy+vElUEDkrpZYczROZ6GUcx70=
|
| |
+ -----END RSA PRIVATE KEY-----
|
| |
+ """
|
| |
+
|
| |
+ USER_DATA = """#cloud-config
|
| |
+ users:
|
| |
+ - default
|
| |
+ - name: root
|
| |
+ groups: sudo
|
| |
+ shell: /bin/bash
|
| |
+ ssh_authorized_keys:
|
| |
+ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLXmRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2KWc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qtksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp standard-test-qcow2
|
| |
+ ssh_pwauth: True
|
| |
+ chpasswd:
|
| |
+ list: |
|
| |
+ root:foobar
|
| |
+ expire: False
|
| |
+ runcmd:
|
| |
+ - mkfs.ext4 /dev/sdb
|
| |
+ - mount /dev/sdb /usr/local
|
| |
+ - sudo mount -o remount,rw /usr
|
| |
+ - mkdir /expected /output
|
| |
+ """
|
| |
+
|
| |
+ def main(argv):
|
| |
+ parser = argparse.ArgumentParser(description="Inventory for a QCow2 test image")
|
| |
+ parser.add_argument("--list", action="store_true", help="Verbose output")
|
| |
+ parser.add_argument('--host', help="Get host variables")
|
| |
+ parser.add_argument("subjects", nargs="*", default=shlex.split(os.environ.get("TEST_SUBJECTS", "")))
|
| |
+ opts = parser.parse_args()
|
| |
+
|
| |
+ try:
|
| |
+ if opts.host:
|
| |
+ data = host(opts.host)
|
| |
+ else:
|
| |
+ data = list(opts.subjects)
|
| |
+ sys.stdout.write(json.dumps(data, indent=4, separators=(',', ': ')))
|
| |
+ except RuntimeError as ex:
|
| |
+ sys.stderr.write("{0}: {1}\n".format(os.path.basename(sys.argv[0]), str(ex)))
|
| |
+ return 1
|
| |
+
|
| |
+ return 0
|
| |
+
|
| |
+
|
| |
+ def list(subjects):
|
| |
+ hosts = []
|
| |
+ variables = {}
|
| |
+ for subject in subjects:
|
| |
+ if subject.endswith((".qcow2", ".qcow2c")):
|
| |
+ vars = host(subject)
|
| |
+ if vars:
|
| |
+ hosts.append(subject)
|
| |
+ variables[subject] = vars
|
| |
+ return {"localhost": {"hosts": hosts, "vars": {}}, "subjects": {"hosts": hosts, "vars": {}},
|
| |
+ "_meta": {"hostvars": variables}}
|
| |
+
|
| |
+
|
| |
+ def start_qemu(image, cloudinit, log, disk_directory=None, disk_size=None, portrange=(2222, 5555)):
|
| |
+ for port in xrange(*portrange):
|
| |
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| |
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| |
+ try:
|
| |
+ sock.bind(("127.0.0.3", port))
|
| |
+
|
| |
+ if disk_size:
|
| |
+ return subprocess.Popen(["/usr/bin/qemu-system-x86_64", "-m", "1024", image,
|
| |
+ "-enable-kvm", "-snapshot", "-cdrom", cloudinit,
|
| |
+ "-hdb", disk_directory,
|
| |
+ "-net", "nic,model=virtio", "-net",
|
| |
+ "user,hostfwd=tcp:127.0.0.3:{0}-:22".format(port),
|
| |
+ "-device", "isa-serial,chardev=pts2", "-chardev", "file,id=pts2,path=" + log,
|
| |
+ "-display", "none"], stdout=open(os.devnull, 'w')), port
|
| |
+ else:
|
| |
+ return subprocess.Popen(["/usr/bin/qemu-system-x86_64", "-m", "1024", image,
|
| |
+ "-enable-kvm", "-snapshot", "-cdrom", cloudinit,
|
| |
+ "-net", "nic,model=virtio", "-net",
|
| |
+ "user,hostfwd=tcp:127.0.0.3:{0}-:22".format(port),
|
| |
+ "-device", "isa-serial,chardev=pts2", "-chardev", "file,id=pts2,path=" + log,
|
| |
+ "-display", "none"], stdout=open(os.devnull, 'w')), port
|
| |
+ except IOError:
|
| |
+ pass
|
| |
+ finally:
|
| |
+ sock.close()
|
| |
+ else:
|
| |
+ raise RuntimeError("unable to find free local port to map SSH to")
|
| |
+
|
| |
+
|
| |
+ def host(image):
|
| |
+ null = open(os.devnull, 'w')
|
| |
+
|
| |
+ try:
|
| |
+ tty = os.open("/dev/tty", os.O_WRONLY)
|
| |
+ os.dup2(tty, 2)
|
| |
+ except OSError:
|
| |
+ tty = None
|
| |
+ pass
|
| |
+
|
| |
+ # A directory for temporary stuff
|
| |
+ directory = tempfile.mkdtemp(prefix="inventory-cloud")
|
| |
+ identity = os.path.join(directory, "identity")
|
| |
+ with open(identity, 'w') as f:
|
| |
+ f.write(IDENTITY)
|
| |
+ os.chmod(identity, 0o600)
|
| |
+ metadata = os.path.join(directory, "meta-data")
|
| |
+ with open(metadata, 'w') as f:
|
| |
+ f.write("")
|
| |
+ userdata = os.path.join(directory, "user-data")
|
| |
+ with open(userdata, 'w') as f:
|
| |
+ f.write(USER_DATA)
|
| |
+
|
| |
+ # Create additional disk
|
| |
+ disk_size = None
|
| |
+ disk_directory = None
|
| |
+ try:
|
| |
+ disk_size = os.environ.get("EXTEND_DISK_SIZE")
|
| |
+ if disk_size:
|
| |
+ sys.stderr.write("\nCreate additional cloud init disk DISK SIZE {}\n".format(disk_size))
|
| |
+
|
| |
+ disk_directory = "{}/atomic-host-disk2-{}".format(directory, disk_size)
|
| |
+ subprocess.check_call(["qemu-img", "create", "-f", "qcow2", disk_directory, disk_size], stdout=null)
|
| |
+
|
| |
+ except KeyError:
|
| |
+ sys.stderr.write("\nCouldn't create additional cloud init disk DISK SIZE\n")
|
| |
+ pass
|
| |
+
|
| |
+ # Create our cloud init so we can log in
|
| |
+ cloudinit = os.path.join(directory, "cloud-init.iso")
|
| |
+ subprocess.check_call(["/usr/bin/genisoimage", "-input-charset", "utf-8",
|
| |
+ "-volid", "cidata", "-joliet", "-rock", "-quiet",
|
| |
+ "-output", cloudinit, userdata, metadata], stdout=null)
|
| |
+
|
| |
+ # Determine if virtual machine should be kept available for diagnosis after completion
|
| |
+ try:
|
| |
+ diagnose = distutils.util.strtobool(os.getenv("TEST_DEBUG", "0"))
|
| |
+ except ValueError:
|
| |
+ diagnose = 0
|
| |
+
|
| |
+ sys.stderr.write("Launching virtual machine for {0}\n".format(image))
|
| |
+
|
| |
+ # And launch the actual VM
|
| |
+ artifacts = os.environ.get("TEST_ARTIFACTS", os.path.join(os.getcwd(), "artifacts"))
|
| |
+ try:
|
| |
+ os.makedirs(artifacts)
|
| |
+ except OSError as exc:
|
| |
+ if exc.errno != errno.EEXIST or not os.path.isdir(artifacts):
|
| |
+ raise
|
| |
+ log = os.path.join(artifacts, "{0}.log".format(os.path.basename(image)))
|
| |
+
|
| |
+ proc = None # for failure detection
|
| |
+ cpe = None # for exception scoping
|
| |
+ for tries in xrange(0, 5):
|
| |
+ try:
|
| |
+ proc, port = start_qemu(image, cloudinit, log, disk_directory=disk_directory, disk_size=disk_size)
|
| |
+ break
|
| |
+ except subprocess.CalledProcessError as cpe:
|
| |
+ time.sleep(1)
|
| |
+ continue
|
| |
+ if proc is None:
|
| |
+ raise RuntimeError("Could not launch VM for qcow2 image"
|
| |
+ " '{0}':{1}".format(image, cpe.output))
|
| |
+
|
| |
+ # The variables
|
| |
+ variables = {"ansible_ssh_port": "{0}".format(port),
|
| |
+ "ansible_ssh_host": "127.0.0.3",
|
| |
+ "ansible_ssh_user": "root",
|
| |
+ "ansible_ssh_pass": "foobar",
|
| |
+ "ansible_ssh_private_key_file": identity,
|
| |
+ "ansible_ssh_common_args": "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"}
|
| |
+
|
| |
+ # Write out a handy inventory file, for our use and for debugging
|
| |
+ args = " ".join(["{0}='{1}'".format(*item) for item in variables.items()])
|
| |
+ inventory = os.path.join(directory, "inventory")
|
| |
+ with open(inventory, "w") as f:
|
| |
+ f.write("[subjects]\nlocalhost {1}\n".format(image, args))
|
| |
+
|
| |
+ # Wait for ssh to come up
|
| |
+ ping = ["/usr/bin/ansible", "--inventory", inventory, "localhost", "--module-name", "raw", "--args", "/bin/true"]
|
| |
+
|
| |
+ for tries in xrange(0, 30):
|
| |
+ try:
|
| |
+ (pid, ret) = os.waitpid(proc.pid, os.WNOHANG)
|
| |
+ if pid != 0:
|
| |
+ raise RuntimeError("qemu failed to launch qcow2 image: {0}".format(image))
|
| |
+ subprocess.check_call(ping, stdout=null, stderr=null)
|
| |
+ break
|
| |
+ except subprocess.CalledProcessError:
|
| |
+ time.sleep(3)
|
| |
+ else:
|
| |
+ # Kill the qemu process
|
| |
+ try:
|
| |
+ os.kill(proc.pid, signal.SIGTERM)
|
| |
+ except OSError:
|
| |
+ pass
|
| |
+ raise RuntimeError("could not access launched qcow2 image: {0}".format(image))
|
| |
+
|
| |
+ # Process of our parent
|
| |
+ ppid = os.getppid()
|
| |
+
|
| |
+ child = os.fork()
|
| |
+ if child:
|
| |
+ return variables
|
| |
+
|
| |
+ # Daemonize and watch the processes
|
| |
+ os.chdir("/")
|
| |
+ os.setsid()
|
| |
+ os.umask(0)
|
| |
+
|
| |
+ if tty is None:
|
| |
+ tty = null.fileno()
|
| |
+
|
| |
+ # Duplicate standard input to standard output and standard error.
|
| |
+ os.dup2(null.fileno(), 0)
|
| |
+ os.dup2(tty, 1)
|
| |
+ os.dup2(tty, 2)
|
| |
+
|
| |
+ # Now wait for the parent process to go away, then kill the VM
|
| |
+ while True:
|
| |
+ time.sleep(3)
|
| |
+ try:
|
| |
+ os.kill(ppid, 0)
|
| |
+ os.kill(proc.pid, 0)
|
| |
+ except OSError:
|
| |
+ break # Either of the processes no longer exist
|
| |
+
|
| |
+ if diagnose:
|
| |
+ sys.stderr.write("\n")
|
| |
+ sys.stderr.write("DIAGNOSE: ssh -p {0} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
| |
+ "root@{1} # password: {2}\n".format(port, "127.0.0.3", "foobar"))
|
| |
+ sys.stderr.write("DIAGNOSE: export ANSIBLE_INVENTORY={0}\n".format(inventory))
|
| |
+ sys.stderr.write("DIAGNOSE: kill {0} # when finished\n".format(os.getpid()))
|
| |
+
|
| |
+ def _signal_handler(*args):
|
| |
+ sys.stderr.write("\nDIAGNOSE ending...\n")
|
| |
+
|
| |
+ signal.signal(signal.SIGTERM, _signal_handler)
|
| |
+ signal.pause()
|
| |
+
|
| |
+ # Kill the qemu process
|
| |
+ try:
|
| |
+ os.kill(proc.pid, signal.SIGTERM)
|
| |
+ except OSError:
|
| |
+ pass
|
| |
+
|
| |
+ shutil.rmtree(directory)
|
| |
+ sys.exit(0)
|
| |
+
|
| |
+
|
| |
+ if __name__ == '__main__':
|
| |
+ sys.exit(main(sys.argv))
|
| |