vmtool/vm.py

491 lines
13 KiB
Python
Executable File

#!/usr/bin/env python
"""vm - simple libvirt wrapper
usage: vm [--version] [--help] [options] <command> [<args>...]
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['<name>'],
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 <name> [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['<command>'].lower()
argv = [cmd] + args['<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)