#!/usr/bin/env python """vm - simple libvirt wrapper usage: vm [--version] [--help] [options] [...] Available commands are: create Create new VM from cloud image ip:list List available OVH IPs ip:sync Create missing vMACs for OVH IPs Available options are: -l, --libvirt=URI Use provided URI to connect to libvirt host """ from __future__ import print_function __version__ = '0.1' import logging logging.getLogger('ovh.vendor').setLevel(logging.WARNING) logformat = '%(asctime)s %(name)s %(levelname)-7s %(message)s' logdatefmt = '%Y-%m-%d %H:%M:%S' loglevel = logging.INFO try: import coloredlogs coloredlogs.DEFAULT_LEVEL_STYLES['info'] = {'color': 'blue'} coloredlogs.install(level=loglevel, fmt=logformat, datefmt=logdatefmt) except: logging.basicConfig(level=loglevel, format=logformat, datefmt=logdatefmt) logger = logging.getLogger(__name__) from docopt import docopt import subprocess import sys import tempfile import string import random import os import socket import jinja2 import libvirt import ovhtool import ovh import ovh.exceptions from collections import namedtuple BASE = os.path.dirname(os.path.realpath(__file__)) config = { 'vg': '/dev/vg', 'bridge': 'br0', 'ds-directory': '/datasource', 'templates-directory': '/templates', } def get_local_ip(): return [ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")][0] def gen_password(length=8, chars=string.letters + string.digits): return ''.join(random.choice(chars) for _ in range(length)) def replace_ip(ip, last): return '.'.join(ip.split('.')[:-1] + [last]) def status(_): logger.info('*** ' + _) def run(_, *args): status('%s %r' % (_, args)) subprocess.call(args) def to_mb(value): v = int(value.lower()[:-1]) if value.lower()[-1] == 'g': return v * 1024 elif value.lower()[-1] == 'm': return v else: return int(value.lower()) j2 = jinja2.Environment( loader=jinja2.FileSystemLoader(os.path.join(BASE, 'templates'))) j2.filters['ipreplace'] = replace_ip j2.filters['megabytes'] = to_mb class Instance(object): ovh_ips = None def __init__(self, name, cpus, memory, storage, template, ip=None, mac=None, dns=None, password=None, libvirt_conn=None): # General self.name = name self.cpus = cpus self.memory = memory self.storage = storage self.template = template # Network self.ip = ip or self.generate_ip() self.mac = mac or self.generate_mac() self.dns = dns if not self.ip: raise Exception('No available IP') if not self.mac: raise Exception('No available MAC') self.user = 'ubuntu' self.password = password if not self.password: self.password = gen_password() self._libvirt_conn = libvirt_conn def create(self): self.prepare_datasource() self.prepare_storage() self.prepare_instance() def prepare_datasource(self): status('Create metadata file') tempdir = tempfile.mkdtemp() netconfig_path = os.path.join(tempdir, 'network-config') metadata_path = os.path.join(tempdir, 'meta-data') userdata_path = os.path.join(tempdir, 'user-data') with open(metadata_path, 'w') as fd: fd.write(self.metadata) with open(userdata_path, 'w') as fd: fd.write(self.userdata) with open(netconfig_path, 'w') as fd: fd.write(self.netconfig) run('Prepare metadata image', 'cloud-localds', self.ds_path, userdata_path, metadata_path, '--network-config', netconfig_path) def prepare_storage(self): run('Create LV', 'lvcreate', config['vg'], '--name', self.name, '--size', self.storage) run('Clone template', 'qemu-img', 'convert', self.template_path, self.storage_path) def prepare_instance(self): conn = self.libvirt_conn dom = conn.defineXML(self.libvirt_definition) dom.create() @property def libvirt_conn(self): if not self._libvirt_conn: self._libvirt_conn = libvirt.open(None) return self._libvirt_conn def generate_ip(self): '''Generates IP address when none was provided by user''' if not self.ovh_ips: self.ovh_ips = ovhtool.list_ips() free_ips = filter(lambda v: not v.domain, self.ovh_ips.values()) return free_ips[0].ip if free_ips else None def generate_mac(self): '''Generates MAC address when none was provided''' if not self.ovh_ips: self.ovh_ips = ovhtool.list_ips() ips = filter(lambda v: v.ip == self.ip, self.ovh_ips.values()) return ips[0].mac if ips else None @classmethod def from_args(cls, args): return cls( name=args[''], cpus=args['--cpus'], memory=args['--mem'], ip=args['--ip'], mac=args['--mac'], storage=args['--storage'], template=args['--template'], dns=args['--dns'], password=args['--password'], ) @property def metadata(self): return j2.get_template('meta-data').render(**self.template_context) @property def userdata(self): return j2.get_template('user-data').render(**self.template_context) @property def netconfig(self): return j2.get_template('network-config').render(**self.template_context) @property def libvirt_definition(self): return j2.get_template('libvirt.xml').render(**self.template_context) @property def storage_path(self): return '%s/%s' % (config['vg'], self.name) @property def ds_path(self): return '%s/%s-ds.img' % (config['ds-directory'], self.name) @property def template_path(self): return '%s/%s.img' % (config['templates-directory'], self.template) @property def template_context(self): return { 'instance': self, 'host_ip': get_local_ip(), 'config': config, } class CommandException(Exception): pass def cmd_create(args): '''usage: vm create [options] Create a virtual machine -t, --template=NAME Choose template [default: xenial] -p, --password=PASS Password for root user -s, --storage=SIZE Storage size [default: 10G] -m, --mem=MEM Memory size [default: 2G] -c, --cpus=CPU VCPUs count [default: 1] -n, --ip=IP Select IP -M, --mac=MAC Select MAC address -d, --dns=DNS DNS [default: 8.8.8.8 8.8.4.4] # TODO ''' instance = Instance.from_args(args) instance.create() print('''Finished! Template: {0.template} IP: {0.ip} Username: {0.user} Password: {0.password}'''.format(instance)) subprocess.call(['ping', '-n', '-c', '1', '-w', '240', instance.ip]) def cmd_iplist(args): '''usage: vm ip:list List available OVH IPs ''' ips = ovhtool.list_ips() for _, ip in ips.items(): hostname = ip.domain.name() if ip.domain else '' print('%15s | %17s | %s' % (ip.ip, ip.mac, hostname)) def cmd_ipsync(args): '''usage: vm ip:sync Create missing vMACs for available IPs ''' client = ovh.Client() server = ovhtool.server_detect(client) ips = ovhtool.list_ips(client=client) for ip in ips.values(): if ip.mac is None: try: logger.info('Missing MAC for %s, creating', ip.ip) resp = client.post('/dedicated/server/%s/virtualMac' % (server,), ipAddress=ip.ip, type='ovh', virtualMachineName='vm') logger.info('Result: %r', resp) except: logger.exception('Failed!') def cmd_help(args): '''usage: vm help ey? ''' print(__doc__) return 0 from xml.dom import minidom DomainInfo = namedtuple('DomainInfo', [ 'state', 'max_memory', 'memory', 'vcpus', 'cpu_time' ]) BlockInfo = namedtuple('BlockInfo', [ 'capacity', 'allocation', 'physical' ]) class LibvirtInterface(object): def __init__(self, elm): self.elm = elm @property def type(self): return self.elm.getAttribute('type') @property def mac(self): return self.elm.getElementsByTagName('mac')[0].getAttribute('address') @property def alias(self): return self.elm.getElementsByTagName('alias')[0].getAttribute('name') @property def source(self): return self.elm.getElementsByTagName('source')[0].getAttribute('bridge') class LibvirtDisk(object): def __init__(self, elm, dom): self.elm = elm self.domain = dom @property def type(self): return self.elm.getAttribute('type') @property def source(self): src = self.elm.getElementsByTagName('source') if src: return src[0].getAttribute('dev') @property def info(self): if self.source: return BlockInfo(*self.domain.blockInfo(self.source)) @property def present(self): return bool(self.source) class LibvirtDomain(object): _xml = None def __init__(self, dom, conn=None): self.domain = dom self.conn = conn @property def xml(self): if not self._xml: self._xml = minidom.parseString(self.domain.XMLDesc(0)) return self._xml @property def info(self): return DomainInfo(*self.domain.info()) @property def name(self): return self.domain.name() @property def interfaces(self): for interface in self.xml.getElementsByTagName('interface'): yield LibvirtInterface(interface) @property def disks(self): for disk in self.xml.getElementsByTagName('disk'): yield LibvirtDisk(disk, self.domain) def cmd_list(args): '''usage: vm list List existing virtual machines with a summary ''' import terminaltables from termcolor import colored global libvirt_conn def domains_list(): state_colors = { 1: 'green', 3: 'yellow', 5: 'red', } yield ['Name', 'RAM', 'vCPUs', 'IP', 'Storage'] mem_sum = 0 for dom in libvirt_conn.listAllDomains(): state, max_mem, mem, vcpus, cputime = dom.info() d = LibvirtDomain(dom) formatted_mem = '%dM' % (mem/1024) if mem != max_mem: formatted_mem += colored(' (%dM)' % (max_mem/1024), 'grey') mem_sum += mem disk_info = [] for disk in d.disks: if disk.type == 'block' and disk.present: disk_info.append( ('%dG ' + colored('(%s)', 'grey')) % ( disk.info.physical/(1024*1024*1024), disk.source ) ) yield [ colored(d.name, state_colors.get(state, None)), formatted_mem, vcpus, '', ', '.join(disk_info) ] yield [] yield ['', '%dM' % (mem_sum/1024), '', '', ''] table = terminaltables.SingleTable(list(domains_list())) table.justify_columns={ 0: 'right', 1: 'right', 2: 'right' } print(table.table) commands = { 'create': cmd_create, 'list': cmd_list, 'ip:list': cmd_iplist, 'ip:sync': cmd_ipsync, 'help': cmd_help, } libvirt_conn = None def main(gargv): global libvirt_conn args = docopt(__doc__, version='vm '+__version__, options_first=True, argv=gargv) libvirt_conn = libvirt.open(args['--libvirt']) cmd = args[''].lower() argv = [cmd] + args[''] if cmd not in commands: raise CommandException('Command does not exist') return commands[cmd](docopt(commands[cmd].__doc__, argv=argv)) if __name__ == '__main__': try: exit(main(sys.argv[1:])) except (ovh.exceptions.InvalidCredential, ovh.exceptions.NotCredential): req = ovh.Client().request_consumerkey([ {'method': 'GET', 'path': '/ip'}, {'method': 'GET', 'path': '/dedicated/server'}, {'method': 'GET', 'path': '/dedicated/server/*/virtualMac'}, {'method': 'GET', 'path': '/dedicated/server/*/virtualMac/*/virtualAddress'}, {'method': 'POST', 'path': '/dedicated/server/*/virtualMac'}, ]) print(req, file=sys.stderr) exit(1) except CommandException as exc: print(exc, file=sys.stderr) exit(1) except KeyboardInterrupt: print("KeyboardInterrupt caught, exiting", file=sys.stderr) exit(1)