Initial vmtool/ovhtool commit
This commit is contained in:
commit
9d721349c4
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.py[co]
|
||||
ovh.conf
|
9
ovh.conf.dist
Normal file
9
ovh.conf.dist
Normal file
@ -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
|
7
ovhgrant.py
Normal file
7
ovhgrant.py
Normal file
@ -0,0 +1,7 @@
|
||||
import ovh
|
||||
|
||||
client = ovh.Client()
|
||||
print(client.request_consumerkey([
|
||||
{'method': 'GET', 'path': '/*'},
|
||||
{'method': 'POST', 'path': '/*'},
|
||||
]))
|
92
ovhtool.py
Normal file
92
ovhtool.py
Normal file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
'''ovhtool - OVH IP address management tool
|
||||
usage: ovhtool <command>
|
||||
|
||||
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 '<OVHIP {0.ip} {0.mac} {0.domain}>'.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['<command>'] == 'list':
|
||||
ips = list_ips()
|
||||
for _, ip in ips.items():
|
||||
print '%15s | %17s | %s' % (ip.ip, ip.mac, ip.domain)
|
||||
elif args['<command>'] == '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!')
|
||||
|
44
templates/libvirt.xml
Normal file
44
templates/libvirt.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<domain type='kvm'>
|
||||
<name>{{ instance.name }}</name>
|
||||
<vcpu>{{ instance.cpus }}</vcpu>
|
||||
<currentMemory unit='MiB'>{{ instance.memory|megabytes }}</currentMemory>
|
||||
<memory unit='MiB'>{{ instance.memory|megabytes }}</memory>
|
||||
<os>
|
||||
<type>hvm</type>
|
||||
<boot dev='hd'/>
|
||||
</os>
|
||||
<features>
|
||||
<acpi/>
|
||||
<apic/>
|
||||
<pae/>
|
||||
</features>
|
||||
<devices>
|
||||
<interface type='bridge'>
|
||||
<mac address='{{ instance.mac }}' />
|
||||
<source bridge='{{ config.bridge }}'/>
|
||||
<filterref filter='clean-traffic'>
|
||||
<parameter name='IP' value='{{ instance.ip }}' />
|
||||
<parameter name='MAC' value='{{ instance.mac }}' />
|
||||
</filterref>
|
||||
<model type='virtio'/>
|
||||
</interface>
|
||||
<serial type='pty'>
|
||||
<source path='/dev/pts/3'/>
|
||||
<target port='0'/>
|
||||
</serial>
|
||||
<graphics type='vnc' autoport='yes' listen='127.0.0.1'>
|
||||
<listen type='address' address='127.0.0.1'/>
|
||||
</graphics>
|
||||
<video/>
|
||||
<disk type='block' device='disk'>
|
||||
<driver name='qemu' type='raw' />
|
||||
<source dev='{{ instance.storage_path }}'/>
|
||||
<target dev='vda' bus='virtio' />
|
||||
</disk>
|
||||
<disk type='file' device='disk'>
|
||||
<driver name='qemu' type='raw' />
|
||||
<source file='{{ instance.ds_path }}' />
|
||||
<target dev='vdb' bus='virtio' />
|
||||
</disk>
|
||||
</devices>
|
||||
</domain>
|
3
templates/meta-data
Normal file
3
templates/meta-data
Normal file
@ -0,0 +1,3 @@
|
||||
dsmode: local
|
||||
hostname: {{ instance.name }}
|
||||
local-hostname: {{ instance.name }}
|
20
templates/network-config
Normal file
20
templates/network-config
Normal file
@ -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') }}
|
8
templates/user-data
Normal file
8
templates/user-data
Normal file
@ -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 ]
|
248
vm.py
Executable file
248
vm.py
Executable file
@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python
|
||||
"""vm - simple libvirt wrapper
|
||||
usage: vm [--version] [--help] <command> [<args>...]
|
||||
|
||||
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['<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', instance.ip])
|
||||
|
||||
|
||||
commands = {
|
||||
'create': cmd_create,
|
||||
}
|
||||
|
||||
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 CommandException as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
exit(1)
|
||||
except KeyboardInterrupt:
|
||||
print("KeyboardInterrupt caught, exiting", file=sys.stderr)
|
||||
exit(1)
|
Loading…
x
Reference in New Issue
Block a user