├── flavors ├── __init__.py ├── micro.py └── small.py ├── settings ├── __init__.py ├── settings.py.dist └── instance.py.dist ├── templates ├── meta-data.j2 └── user-data.j2 ├── requirements.txt ├── .gitignore ├── README.md └── proxvm-deploy.py /flavors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/meta-data.j2: -------------------------------------------------------------------------------- 1 | instance-id: {{ instance.vmid }} 2 | local-hostname: {{ instance.name }} 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==2.7.2 2 | MarkupSafe==0.19 3 | ecdsa==0.11 4 | paramiko==1.12.2 5 | proxmoxer==0.1.4 6 | pycrypto==2.6.1 7 | requests==2.2.1 8 | wsgiref==0.1.2 9 | -------------------------------------------------------------------------------- /flavors/micro.py: -------------------------------------------------------------------------------- 1 | # instance flavor 2 | flavor = {} 3 | flavor['sockets'] = '1' 4 | flavor['cores'] = '1' 5 | flavor['balloon'] = '0' 6 | flavor['memory'] = '256' 7 | flavor['root_size'] = '3G' 8 | -------------------------------------------------------------------------------- /flavors/small.py: -------------------------------------------------------------------------------- 1 | # instance flavor 2 | flavor = {} 3 | flavor['sockets'] = '1' 4 | flavor['cores'] = '1' 5 | flavor['balloon'] = '0' 6 | flavor['memory'] = '512' 7 | flavor['root_size'] = '10G' 8 | -------------------------------------------------------------------------------- /settings/settings.py.dist: -------------------------------------------------------------------------------- 1 | # proxmox settings 2 | proxmox = {} 3 | 4 | proxmox['host'] = 'proxmox' 5 | proxmox['user'] = 'root@pam' 6 | proxmox['password'] = 'your_proxmox_password' 7 | proxmox['images'] = '/var/lib/vz/images/' 8 | proxmox['verify_ssl'] = False 9 | proxmox['node'] = 'proxmox' 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # Generated files 56 | user-data 57 | meta-data 58 | *seed.raw 59 | 60 | # Sensitive data 61 | settings/settings.py 62 | settings/instance.py 63 | -------------------------------------------------------------------------------- /templates/user-data.j2: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | hostname: {{ instance.name }} 3 | locale: {{ instance.locale }} 4 | timezone: {{ instance.timezone }} 5 | 6 | resize_rootfs: {{ instance.resize_rootfs }} 7 | 8 | write_files: 9 | - path: /etc/default/keyboard 10 | content: | 11 | XKBMODEL="pc105" 12 | XKBLAYOUT="{{ instance.kb_layout }}" 13 | XKBVARIANT="intl" 14 | XKBOPTIONS="" 15 | permissions: '0644' 16 | owner: root:root 17 | 18 | {% if instance.ifs -%} 19 | write_files: 20 | {% for if in instance.ifs -%} 21 | - path: /etc/network/interfaces.d/{{ if.name }}.cfg 22 | content: | 23 | auto {{ if.name }} 24 | iface {{ if.name }} inet {{ if.type }} 25 | {% if if.type == 'static' -%} 26 | address {{ if.address }} 27 | network {{ if.network }} 28 | netmask {{ if.netmask }} 29 | broadcast {{ if.broadcast }} 30 | gateway {{ if.gateway }} 31 | dns-nameservers {% for dns in if.dnss %}{{ dns }}{% if not loop.last %} {% endif %}{% endfor %}{% endif %} 32 | permissions: '0644' 33 | owner: root:root 34 | {% endfor -%} 35 | {% endif %} 36 | {% if instance.users -%} 37 | users: 38 | {% for user in instance.users -%} 39 | - name: {{ user.name }} 40 | {% if user.groups -%} 41 | groups: {% for group in user.groups %}{{ group }}{% if not loop.last %},{% endif %}{% endfor %} 42 | {% endif -%} 43 | sudo: ALL=(ALL) NOPASSWD:ALL 44 | shell: {{ user.shell }} 45 | lock-passwd: False 46 | {% if user.ssh_keys -%} 47 | ssh-authorized-keys: 48 | {% for ssh_key in user.ssh_keys -%} 49 | - {{ ssh_key }} 50 | {% endfor -%} 51 | {% endif -%} 52 | {% endfor -%} 53 | {% endif %} 54 | ssh_pwauth: {{ instance.ssh_pass_auth }} 55 | package_update: {{ instance.apt_update }} 56 | package_upgrade: {{ instance.apt_upgrade }} 57 | 58 | byobu_by_default: system 59 | 60 | {% if instance.packages -%} 61 | packages: 62 | {%for package in instance.packages -%} 63 | - {{ package }} 64 | {% endfor -%} 65 | {% endif -%} 66 | 67 | {% if instance.runcmds -%} 68 | runcmd: 69 | {%for runcmd in instance.runcmds -%} 70 | - {{ runcmd }} 71 | {% endfor -%} 72 | {% endif -%} 73 | 74 | power_state: 75 | mode: reboot 76 | message: Instance ready, rebooting... 77 | -------------------------------------------------------------------------------- /settings/instance.py.dist: -------------------------------------------------------------------------------- 1 | # instance data 2 | instance = {} 3 | 4 | ## instance default 5 | instance['net'] = 'virtio,bridge=vmbr0' 6 | instance['hd_root'] = 'local:%s/vm-%s-disk-1.qcow2' 7 | instance['lvm_root'] = '/dev/vg0/vm-%s-disk-1' 8 | instance['hd_seed'] = 'local:%s/vm-%s-seed.raw' 9 | 10 | ## instance customization 11 | ### os 12 | instance['os'] = 'trusty-server-cloudimg-amd64-disk1.qcow2' 13 | 14 | ### general 15 | instance['locale'] = 'en_US.UTF-8' 16 | instance['timezone'] = 'Europe/Rome' 17 | instance['kb_layout'] = 'it' 18 | instance['resize_rootfs'] = 'True' 19 | 20 | ### network interfaces 21 | instance['ifs'] = [] 22 | instance['ifs'].append({}) 23 | instance['ifs'][0]['name'] = 'eth0' 24 | instance['ifs'][0]['type'] = 'static' 25 | instance['ifs'][0]['address'] = '192.168.42.%s' 26 | instance['ifs'][0]['network'] = '192.168.42.0' 27 | instance['ifs'][0]['netmask'] = '255.255.255.0' 28 | instance['ifs'][0]['broadcast'] = '192.168.42.255' 29 | instance['ifs'][0]['gateway'] = '192.168.42.1' 30 | instance['ifs'][0]['dnss'] = [] 31 | instance['ifs'][0]['dnss'].append('192.168.42.1') 32 | 33 | ### users 34 | instance['users'] = [] 35 | instance['users'].append({}) 36 | instance['users'][0]['name'] = 'libersoft' 37 | instance['users'][0]['groups'] = ['adm', 'users'] 38 | instance['users'][0]['shell'] = '/bin/bash' 39 | instance['users'][0]['ssh_keys'] = ['ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/CnT9nN/acqX13EPMDKUFuKng1rZ3Jc47BiGmBg+ESFGQxltzG48kdHp7FievbtLGhP1igG45XNINkS1ySBGAjG/QhlGgUewS2OXGmlFS/GvkcFDiSyBjL2Mg0xjnPSa29P9F4zlp1txInZDhsAAq/fVVWMTS8rJTc+D8M5tY0eK41f9blsImHvzhCcNZNAzzxi5iYbt0ayObp0q3OcODRBxivmSO/h52sULbF/T+CLMwUacQhOQl3SMuxrL048vGJGlS6ABI9feY3q8bVnc0e1c9pKWEbQ4x8/ScO1r2iD9Wn5VG60aBrXciFL12JBok0A17wx7jmWG9BglywW7J lg@thule', 40 | 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAlztGLOE+8OMs30KNohtMe8QYT3aqhUoleGMSNWmoh3cM1TSdKPzpmhKDu/rZkKl26ax1TUX0VO3nGvAvjjVOs6y6vJtOJcw8fSCJMt7yUHTOx1zHhvnm8Eyc4DN8e+lyNvijA4l6GfpPNOEwC3DvZ/TGZeHqjFJz7Z+5zXueCi0yKQwn3KS6E4CeBe5g/yMJOn7iI8WXPV2Wu9PB2aVzx+87VTnmk6Rrm0vJUzXO3G17phEX+yiMkVQ9ihYnFKbnPe2du9oEkVs8qy7UeIxtwraTdlnuW2pzQI19QlFwvicL2wrq92VihNddbZ5vmMYR28ie4NND+7APypBX5JyYaw== malte@maltebook', 41 | 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuE2M2oBLgLIxIAZFWoJUGs5vqKt6VYKe+6xYii9WdWUuh30CIdleu1yPRXa81feotHcfYiXeBxxlPrRBLw1vF9g/u4l8ezPSDqJ72gfWQVJ9IgSF9PTUZQxnNQmN3fW9bSu7J21EoMFT/LbDwXlm4zGFyhjWkgLHG8mLxRPgPbsY4Nx+rLL4YCz/HUuOBnOu1iVb9rWhzFWX0jewZmGSnKI+jBcSK1IvTzXNvy3hAlc6Aq9siEmFGFqsHd0r1fvn/CnDB4BQgmTnFdBfMTx5ISIids1JqC8UidZWaAKSijNuFL7wQVBnX0o9AnjHXjd2ph+wymX4eM4v6DcHP6C+x lappone@khorne'] 42 | instance['password_expire'] = 'False' 43 | instance['ssh_pass_auth'] = 'True' 44 | 45 | ### packages 46 | instance['apt_update'] = 'False' 47 | instance['apt_upgrade'] = 'False' 48 | instance['packages'] = [] 49 | 50 | ### one time command 51 | instance['runcmds'] = [] 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proxmox-init 2 | 3 | Python script to deploy GNU/Linux cloud images (tested with [Ubuntu](http://cloud-images.ubuntu.com/)), cloud-style, on Proxmox KVM. 4 | Based on Proxmox API Python (proxmoxer) and cloud-init. 5 | Hardware templating of KVM instances is possible by flavors configuration file (inspired by OpenStack flavors). 6 | Only local storage support, both directory and LVM Group. 7 | Cloud-init is linked to Qemu-KVM machines by [NoCloud datasource] (http://cloudinit.readthedocs.org/en/latest/topics/datasources.html#no-cloud). 8 | 9 | ## Installation 10 | 11 | $ git clone https://github.com/libersoft/proxmox-init.git proxmox-init 12 | $ cd proxmox-init 13 | $ pip install requirements.txt 14 | 15 | You need also to install `genisoimage`, needed to create the cloudinit seed datasource. 16 | 17 | ## Configuration 18 | 19 | You have to copy proxmox settings from `settings/settings.py.dist` to `settings/settings.py` and edit it to reflect your current proxmox configuration. 20 | 21 | You can add and modify your hardware flavors in `flavors/`. 22 | In this case you have also to update choices for `--flavor` cmdline switch in the main script. 23 | 24 | You have to copy instance configuration from `settings/instance.py.dist` to `settings/instance.py` and edit it to reflect your need. 25 | 26 | Current format for configuration is quite rough and not very flexible. 27 | 28 | The script assumes you have your ssh key in the authorized_keys of the proxmox node, 29 | for the user specified in the settings file. 30 | 31 | ## Usage 32 | usage: proxvm-deploy.py [-h] --vmid VMID --name NAME [--flavor {micro,small}] 33 | [--storage {dir,lvm}] 34 | 35 | Create a proxmox kvm and cloudinit it. 36 | 37 | optional arguments: 38 | -h, --help show this help message and exit 39 | --vmid VMID, -v VMID Virtual machine id 40 | --name NAME, -n NAME Virtual machine name/hostname 41 | --flavor {micro,small}, -f {micro,small} 42 | Virtual machine flavor 43 | --storage {dir,lvm}, -s {dir,lvm} 44 | Virtual machine storage backend 45 | 46 | ## Status 47 | 48 | Rough working proof of concept. 49 | 50 | ## Possible Improvements 51 | 52 | * Puppet and chef section in cloudinit template. 53 | * Ansible-pull launch on instance customization. 54 | * Support to proxmox remote storage. 55 | * Settings refactoring, especially network interfaces 56 | * Extend cmdline arguments to choose base os image, proxmox node, proxmox host anb other. 57 | * Code reorganization 58 | 59 | ## Random Ideas and maybe 60 | 61 | * Proxmox KVM Ansible module ? 62 | 63 | ## License 64 | 65 | [Gnu General Public License 3.0](https://www.gnu.org/licenses/gpl.html) 66 | 67 | ## Credits 68 | * [Proxmoxer](https://github.com/swayf/proxmoxer) 69 | * [Cloud-init](http://cloudinit.readthedocs.org/en/latest/index.html) 70 | * [Proxmox API](http://pve.proxmox.com/pve2-api-doc/) 71 | * [Local Ubuntu vm provisioning with cloud-init](http://qa.ubuntu.com/2012/06/19/local-ubuntu-vm-provisioning-with-cloud-init/) 72 | * [Copying an image to a physical device](https://en.wikibooks.org/wiki/QEMU/Images#Copying_an_image_to_a_physical_device) 73 | * [API-Create-KVM-with-logical-Volume](http://forum.proxmox.com/threads/12059-API-Create-KVM-with-Logical-Volume) 74 | -------------------------------------------------------------------------------- /proxvm-deploy.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import argparse 4 | from subprocess import call 5 | from importlib import import_module 6 | 7 | from proxmoxer import ProxmoxAPI 8 | from jinja2 import Environment, FileSystemLoader 9 | from paramiko import SSHClient, WarningPolicy 10 | 11 | from settings.settings import proxmox 12 | from settings.instance import instance 13 | 14 | 15 | def instance_customize(instance, vmid, name, flavor_type): 16 | # retrieve flavour 17 | flavor_module = import_module('flavors.%s' % (flavor_type)) 18 | flavor = flavor_module.flavor 19 | 20 | # configure instance custom details 21 | instance['hd_root'] = instance['hd_root'] % (vmid, vmid) 22 | instance['lvm_root'] = instance['lvm_root'] % (vmid) 23 | instance['hd_seed'] = instance['hd_seed'] % (vmid, vmid) 24 | 25 | for ifs in instance['ifs']: 26 | ifs['address'] = ifs['address'] % (vmid) 27 | 28 | instance['vmid'] = vmid 29 | instance['name'] = name 30 | 31 | return instance, flavor 32 | 33 | 34 | def get_proxmox_ssh(proxmox): 35 | proxmox_ssh = SSHClient() 36 | proxmox_ssh.set_missing_host_key_policy(WarningPolicy()) 37 | proxmox_ssh.connect(proxmox['host'], 38 | username=proxmox['user'].split('@')[0]) 39 | 40 | return proxmox_ssh 41 | 42 | 43 | def seed(vmid, instance, proxmox): 44 | # compile seed data 45 | env = Environment(loader=FileSystemLoader('templates')) 46 | 47 | metadata_j2 = env.get_template('meta-data.j2') 48 | with open('meta-data', 'w') as metadata: 49 | metadata.write(metadata_j2.render(instance=instance)) 50 | 51 | userdata_j2 = env.get_template('user-data.j2') 52 | with open('user-data', 'w') as userdata: 53 | userdata.write(userdata_j2.render(instance=instance)) 54 | 55 | # generate seed iso 56 | call(['genisoimage', '-output', 'vm-%s-seed.raw' % (vmid), 57 | '-volid', 'cidata', '-joliet', '-rock', 'user-data', 'meta-data']) 58 | 59 | # sftp seed (paramiko) 60 | source = 'vm-%s-seed.raw' % (vmid) 61 | destination = '%s%s' % (proxmox['images'], 62 | instance['hd_seed'].replace('local:', '')) 63 | 64 | proxmox_ssh = get_proxmox_ssh(proxmox) 65 | 66 | command = 'mkdir -p %s%s' % (proxmox['images'], vmid) 67 | 68 | stdin, stdout, stderr = proxmox_ssh.exec_command(command) 69 | 70 | proxmox_sftp = proxmox_ssh.open_sftp() 71 | proxmox_sftp.put(source, destination) 72 | 73 | proxmox_sftp.close() 74 | proxmox_ssh.close() 75 | 76 | 77 | def dir_volume(vmid, instance, proxmox): 78 | # mv base image to current vmid (paramiko) 79 | command = 'cp -f %s%s %s%s' % (proxmox['images'], 80 | instance['os'], 81 | proxmox['images'], 82 | instance['hd_root'].replace('local:', '')) 83 | proxmox_ssh = get_proxmox_ssh(proxmox) 84 | stdin, stdout, stderr = proxmox_ssh.exec_command(command) 85 | proxmox_ssh.close() 86 | 87 | 88 | def lvm_volume(vmid, instance, proxmox): 89 | 90 | command = 'qemu-img convert -O raw %s%s %s' % (proxmox['images'], 91 | instance['os'], 92 | instance['lvm_root']) 93 | 94 | proxmox_ssh = get_proxmox_ssh(proxmox) 95 | stdin, stdout, stderr = proxmox_ssh.exec_command(command) 96 | proxmox_ssh.close() 97 | 98 | 99 | if __name__ == "__main__": 100 | 101 | # configuring and reading arguments 102 | parser = argparse.ArgumentParser(description='Create a proxmox kvm and \ 103 | cloudinit it.') 104 | 105 | parser.add_argument('--vmid', '-v', type=int, help='Virtual machine id', 106 | required=True) 107 | parser.add_argument('--name', '-n', type=str, 108 | help='Virtual machine name/hostname', 109 | required=True) 110 | parser.add_argument('--flavor', '-f', type=str, 111 | default='small', choices=['micro', 'small'], 112 | help='Virtual machine flavor') 113 | parser.add_argument('--storage', '-s', type=str, 114 | default='dir', choices=['dir', 'lvm'], 115 | help='Virtual machine storage backend') 116 | 117 | args = parser.parse_args() 118 | 119 | vmid = str(args.vmid) 120 | name = args.name 121 | flavor_type = args.flavor 122 | storage_type = args.storage 123 | 124 | # customize flavor 125 | instance, flavor = instance_customize(instance, vmid, name, flavor_type) 126 | 127 | # proxmoxer initialize 128 | proxmox_api = ProxmoxAPI(proxmox['host'], user=proxmox['user'], 129 | password=proxmox['password'], 130 | verify_ssl=proxmox['verify_ssl']) 131 | 132 | node = proxmox_api.nodes(proxmox['node']) 133 | 134 | # create kvm machine 135 | node.qemu.create(vmid=vmid, name=name, sockets=flavor['sockets'], 136 | cores=flavor['cores'], balloon=flavor['balloon'], 137 | memory=flavor['memory'], net0=instance['net']) 138 | 139 | # seeding 140 | seed(vmid, instance, proxmox) 141 | 142 | # set seed iso 143 | node.qemu(vmid).config.set(virtio1=instance['hd_seed']) 144 | 145 | # create root volume 146 | if storage_type == 'dir': 147 | dir_volume(vmid, instance, proxmox) 148 | # set root image 149 | node.qemu(vmid).config.set(virtio0=instance['hd_root']) 150 | # adjust root size 151 | node.qemu(vmid).resize.set(disk='virtio0', size=flavor['root_size']) 152 | elif storage_type == 'lvm': 153 | # initialize lvm volume 154 | node.qemu(vmid).config.set(virtio0='%s:%s' % ('vg0', 155 | flavor['root_size'].replace('G', ''))) 156 | lvm_volume(vmid, instance, proxmox) 157 | 158 | # set boot device 159 | node.qemu(vmid).config.set(bootdisk='virtio0') 160 | 161 | # start virtual machine 162 | node.qemu(vmid).status.start.create() 163 | --------------------------------------------------------------------------------