From 9d721349c47b7d962ca6c392bf56058e51a6c4a9 Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Tue, 31 May 2016 00:08:37 +0200 Subject: [PATCH] Initial vmtool/ovhtool commit --- .gitignore | 2 + ovh.conf.dist | 9 ++ ovhgrant.py | 7 ++ ovhtool.py | 92 +++++++++++++++ templates/libvirt.xml | 44 +++++++ templates/meta-data | 3 + templates/network-config | 20 ++++ templates/user-data | 8 ++ vm | 1 + vm.py | 248 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 434 insertions(+) create mode 100644 .gitignore create mode 100644 ovh.conf.dist create mode 100644 ovhgrant.py create mode 100644 ovhtool.py create mode 100644 templates/libvirt.xml create mode 100644 templates/meta-data create mode 100644 templates/network-config create mode 100644 templates/user-data create mode 120000 vm create mode 100755 vm.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d82c1f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.py[co] +ovh.conf diff --git a/ovh.conf.dist b/ovh.conf.dist new file mode 100644 index 0000000..6046b11 --- /dev/null +++ b/ovh.conf.dist @@ -0,0 +1,9 @@ +[default] +; general configuration: default endpoint +endpoint=soyoustart-eu + +[soyoustart-eu] +; configuration specific to 'soyoustart-eu' endpoint +application_key=APP_KEY +application_secret=APP_SECRET +consumer_key=CONSUMER_KEY diff --git a/ovhgrant.py b/ovhgrant.py new file mode 100644 index 0000000..e9cb080 --- /dev/null +++ b/ovhgrant.py @@ -0,0 +1,7 @@ +import ovh + +client = ovh.Client() +print(client.request_consumerkey([ + {'method': 'GET', 'path': '/*'}, + {'method': 'POST', 'path': '/*'}, + ])) diff --git a/ovhtool.py b/ovhtool.py new file mode 100644 index 0000000..0eb112d --- /dev/null +++ b/ovhtool.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +'''ovhtool - OVH IP address management tool +usage: ovhtool + +Available commands are: + list Lists IP addresses + sync Creates vmac records where missing +''' +import logging + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +import ovh +import json +from collections import namedtuple +from docopt import docopt + +try: + import libvirt +except: + libvirt = None + logger.warning('No libvirt - not using IP availability data') + +from xml.dom import minidom + +current_server = None + +class OVHIP(object): + def __init__(self, ip, mac, domain): + self.ip = ip + self.mac = mac + self.domain = domain + + def __repr__(self): + return ''.format(self) + +def list_ips(server=None, client=None): + domains = {} + + if libvirt: + c = libvirt.openReadOnly(None) + + for domid in c.listDomainsID(): + domain = c.lookupByID(domid) + domains[domid] = [] + + dom = minidom.parseString(domain.XMLDesc(0)) + for interface in dom.getElementsByTagName('interface'): + mac = interface.getElementsByTagName('mac')[0].getAttribute('address') + domains[mac.lower()] = domain + + client = client or ovh.Client() + if not server: + server = client.get('/dedicated/server')[0] + + ips = client.get('/ip?routedTo.serviceName=%s&type=failover' % server) + + # TODO: IP ranges + ips = {v.split('/')[0]: OVHIP(v.split('/')[0], None, None) for v in ips} + + macs = client.get('/dedicated/server/%s/virtualMac' % server) + for mac in macs: + resp = client.get('/dedicated/server/%s/virtualMac/%s/virtualAddress' % + (server, mac))[0] + ips[resp].mac = mac + ips[resp].domain = domains.get(mac.lower()) + + return ips + +if __name__ == "__main__": + args = docopt(__doc__, version='ovhtool 0.1') + if args[''] == 'list': + ips = list_ips() + for _, ip in ips.items(): + print '%15s | %17s | %s' % (ip.ip, ip.mac, ip.domain) + elif args[''] == 'sync': + client = ovh.Client() + server = client.get('/dedicated/server')[0] + ips = 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!') + diff --git a/templates/libvirt.xml b/templates/libvirt.xml new file mode 100644 index 0000000..9f446ac --- /dev/null +++ b/templates/libvirt.xml @@ -0,0 +1,44 @@ + + {{ instance.name }} + {{ instance.cpus }} + {{ instance.memory|megabytes }} + {{ instance.memory|megabytes }} + + hvm + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/meta-data b/templates/meta-data new file mode 100644 index 0000000..a24d337 --- /dev/null +++ b/templates/meta-data @@ -0,0 +1,3 @@ +dsmode: local +hostname: {{ instance.name }} +local-hostname: {{ instance.name }} diff --git a/templates/network-config b/templates/network-config new file mode 100644 index 0000000..d992d07 --- /dev/null +++ b/templates/network-config @@ -0,0 +1,20 @@ +--- +version: 1 +config: +{% for ns in instance.dns.split(' ') %} + - type: nameserver + address: {{ ns }} +{% endfor %} + - type: physical + name: ens3 + subnets: + - control: auto + type: static + address: | + {{ instance.ip }} + post-up route add {{ host_ip|ipreplace('254') }} dev ens3 + post-up route add default gw {{ host_ip|ipreplace('254') }} + post-down route del {{ host_ip|ipreplace('254') }} dev ens3 + post-down route del default gw {{ host_ip|ipreplace('254') }} + netmask: 255.255.255.255 + broadcast: {{ instance.ip|ipreplace('1') }} diff --git a/templates/user-data b/templates/user-data new file mode 100644 index 0000000..93eb05d --- /dev/null +++ b/templates/user-data @@ -0,0 +1,8 @@ +#cloud-config +chpasswd: + list: | + {{ instance.user }}:{{ instance.password }} + expire: False + +bootcmd: + - [ cloud-init-per, once, sshenable, sed, -i, 's/PasswordAuthentication no/PasswordAuthentication yes/g', /etc/ssh/sshd_config ] diff --git a/vm b/vm new file mode 120000 index 0000000..23143ec --- /dev/null +++ b/vm @@ -0,0 +1 @@ +vm.py \ No newline at end of file diff --git a/vm.py b/vm.py new file mode 100755 index 0000000..f958b63 --- /dev/null +++ b/vm.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python +"""vm - simple libvirt wrapper +usage: vm [--version] [--help] [...] + +Available commands are: + create Create new VM from cloud image +""" + +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 + +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 + else: + return v + +j2 = jinja2.Environment(loader=jinja2.FileSystemLoader('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', instance.ip]) + + +commands = { + 'create': cmd_create, + } + +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 CommandException as exc: + print(exc, file=sys.stderr) + exit(1) + except KeyboardInterrupt: + print("KeyboardInterrupt caught, exiting", file=sys.stderr) + exit(1)