vmtool/vm.py

313 lines
8.4 KiB
Python
Executable File

#!/usr/bin/env python
"""vm - simple libvirt wrapper
usage: vm [--version] [--help] <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
"""
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['<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', '-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['<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:
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)