#!/usr/bin/env python """vm - simple libvirt wrapper usage: vm [--version] [--help] [...] Available commands are: create Create new VM from cloud image ip:list List available OVH IPs ip:sync Create missing vMACs for OVH IPs """ from __future__ import print_function __version__ = '0.1' import logging logging.basicConfig(level=logging.DEBUG) 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 BASE = os.path.dirname(os.path.abspath(__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): # 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() 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 = libvirt.open(None) dom = conn.defineXML(self.libvirt_definition) dom.create() 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', '-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 commands = { 'create': cmd_create, 'ip:list': cmd_iplist, 'ip:sync': cmd_ipsync, 'help': cmd_help, } def main(gargv): args = docopt(__doc__, version='vm '+__version__, options_first=True, argv=gargv) 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: 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)