491 lines
13 KiB
Python
Executable File
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)
|