shipyard 7.95 KB
Newer Older
Noe Nieto's avatar
Noe Nieto committed
1 2 3 4 5 6 7
#!/usr/bin/python3
import sys
import os
import argparse
import tempfile
import subprocess
import json
Noe Nieto's avatar
Noe Nieto committed
8
import shutil
9
from pathlib import Path
10 11
import ipaddress

Noe Nieto's avatar
Noe Nieto committed
12 13 14 15

vagrant_template = """
Vagrant.configure("2") do |config|
  config.vm.hostname = "{hostname}"
Noe Nieto's avatar
Noe Nieto committed
16 17 18 19
  config.vm.define :{hostname} do |{hostname}|
    {hostname}.vm.box = "debian/stretch64"
    {hostname}.vm.hostname = "{hostname}"
    {hostname}.vm.provider :libvirt do |libvirt|
Noe Nieto's avatar
Noe Nieto committed
20 21 22 23 24 25
      libvirt.driver = "kvm"
      libvirt.memory = "{ram}"
      libvirt.username = "root"
      libvirt.graphics_type = "spice"
      libvirt.cpus = {cpus}
    end
Noe Nieto's avatar
Noe Nieto committed
26 27 28
    {hostname}.vm.provision "ansible" do |ansible|
      ansible.playbook = "shim.yml"
    end
Noe Nieto's avatar
Noe Nieto committed
29 30 31 32
  end
end
"""

Noe Nieto's avatar
Noe Nieto committed
33 34 35 36 37 38 39 40 41 42 43 44
ansible_shim = """
---
- hosts: all
  become: yes
  tasks:
    - name: Shim file for ansible
      file:
        path: /etc/shipyard_shim.txt
        state: touch

"""

45
XDG_CONFIG_HOME = Path(os.environ['HOME'],'.config','ldh_developer','shipyard')
Noe Nieto's avatar
Noe Nieto committed
46

47

Noe Nieto's avatar
Noe Nieto committed
48 49 50
def only_if_box_exist(func):
    def wrapper(*args, **kwargs):
        hostname = args[0]
51
        vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
Noe Nieto's avatar
Noe Nieto committed
52 53 54 55 56 57 58
        if vagrant_wd.exists():
            func(*args, **kwargs)
        else:
            print("Shipyard error: no vm managed by me has the hostname '{}'".format(hostname))
            exit(1)
    return wrapper

Noe Nieto's avatar
Noe Nieto committed
59

60 61 62 63 64 65 66 67
def is_ipaddress(ip_addr):
    try:
        ipaddress.ip_address(ip_addr)
    except ValueError:
        return False
    return True


68 69 70 71
def print_status():
    """
    Prints the status of all the virtual machines managed by shipyard
    """
Noe Nieto's avatar
Noe Nieto committed
72
    shpyrd_paths = [str(d) for d in XDG_CONFIG_HOME.iterdir() if d.is_dir()]
73
    vgt_db = Path(
Noe Nieto's avatar
Noe Nieto committed
74 75
        os.environ.get('VAGRANT_HOME', str(Path.home().joinpath('.vagrant.d'))),
        'data', 'machine-index', 'index'
76 77 78
    )
    count = 0
    if vgt_db.exists():
79
        print('hostname \t State\t IP Address')
80 81
        db = json.loads(vgt_db.open().read())
        for k, v in db['machines'].items():
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
            vagrant_wd = v['vagrantfile_path']
            if vagrant_wd in shpyrd_paths:
                # get the ip address
                cp = subprocess.run(
                    ['vagrant','ssh-config'], cwd=vagrant_wd,
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    universal_newlines=True
                )
                if cp.returncode != 0:
                    ip_addr = "<N/A>"
                else:
                    for fld in cp.stdout.split():
                        if is_ipaddress(str(fld)):
                            ip_addr = str(fld)
                            break
                print(f"{v['name']} \t {v['state']}\t {ip_addr}")
98 99 100 101 102 103 104 105
                count+=1
    print('Total {} machine(s)'.format(count))


def create_box(hostname, ram, cpus):
    """
    This creates a box using the template
    """
106
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
107 108 109 110 111 112 113 114 115 116 117 118
    if vagrant_wd.exists():
        print("Shipyard error: There's already a vm with that hostname")
        exit(0)
    vagrant_wd.mkdir(parents=True)

    # Generate Vagrantfile
    with vagrant_wd.joinpath('Vagrantfile').open(mode='w') as vfile:
        vfile.write(vagrant_template.format(
            hostname=hostname,
            ram=ram,
            cpus=cpus,
        ))
Noe Nieto's avatar
Noe Nieto committed
119
    # The Ansible shim
120
    vagrant_wd.joinpath('shim.yml').open(mode='w').write(ansible_shim)
121 122 123 124 125

    # Execute vagrant with the generated vagrantfile
    subprocess.run(['vagrant','up'], cwd=vagrant_wd)


Noe Nieto's avatar
Noe Nieto committed
126
@only_if_box_exist
Noe Nieto's avatar
Noe Nieto committed
127
def destroy_vm(hostname):
128 129 130
    """
    Destroy a box, or all
    """
131
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
Noe Nieto's avatar
Noe Nieto committed
132 133 134 135 136
    subprocess.run(['vagrant','destroy'], cwd=vagrant_wd)
    shutil.rmtree(vagrant_wd)


@only_if_box_exist
Noe Nieto's avatar
Noe Nieto committed
137
def run_playbook(hostname, playbook, retry_file=None):
Noe Nieto's avatar
Noe Nieto committed
138
    """
Noe Nieto's avatar
Noe Nieto committed
139
    run an Ansible playbook against this vm
Noe Nieto's avatar
Noe Nieto committed
140
    """
141
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
Noe Nieto's avatar
Noe Nieto committed
142
    inventory_file = vagrant_wd.joinpath('.vagrant', 'provisioners', 'ansible', 'inventory', 'vagrant_ansible_inventory')
Noe Nieto's avatar
Noe Nieto committed
143
    _args = [
Noe Nieto's avatar
Noe Nieto committed
144 145
        'ansible-playbook', '-u', 'vagrant', '-i', f'{inventory_file}',
        f'{Path(playbook).resolve()}'
Noe Nieto's avatar
Noe Nieto committed
146 147 148 149 150 151
    ]

    if retry_file:
        _args.extend(['--limit', f'@{Path(retry_file).resolve()}'])

    subprocess.run(_args, cwd=vagrant_wd)
Noe Nieto's avatar
Noe Nieto committed
152

153

Noe Nieto's avatar
Noe Nieto committed
154 155 156 157 158 159 160 161 162 163
@only_if_box_exist
def start_vm(hostname):
    """
    Starts the vm with the provided hostname
    """
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
    subprocess.run(['vagrant','up'], cwd=vagrant_wd)


@only_if_box_exist
Noe Nieto's avatar
Noe Nieto committed
164
def halt_vm(hostname):
Noe Nieto's avatar
Noe Nieto committed
165 166 167 168 169 170 171
    """
    Stops the vm with the provided hostname
    """
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
    subprocess.run(['vagrant','halt'], cwd=vagrant_wd)


Noe Nieto's avatar
Noe Nieto committed
172 173
@only_if_box_exist
def invoke_shell(hostname):
174
    """
175
    Open a SSH session to the hostname
176
    """
177
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
Noe Nieto's avatar
Noe Nieto committed
178
    subprocess.run(['vagrant','ssh'], cwd=vagrant_wd)
179 180


Noe Nieto's avatar
Noe Nieto committed
181 182 183 184 185 186 187 188
@only_if_box_exist
def which(hostname):
    """
    Print the directory of a vm by it's hostname
    """
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
    print(vagrant_wd)

189

Noe Nieto's avatar
Noe Nieto committed
190 191 192 193 194 195 196 197
@only_if_box_exist
def gio_open_vm(hostname):
    """
    Open the vm directory in nautilis or any other file manager
    """
    vagrant_wd = XDG_CONFIG_HOME.joinpath(hostname)
    subprocess.run(['gio','open', vagrant_wd])

198 199 200 201
COMMANDS = {
    'status': "Prints the status of all the VM's managed by shipyard",
    'create': 'Creates a new VM using vagrant and libvirt (KVM)',
    'up': 'Start VM',
Noe Nieto's avatar
Noe Nieto committed
202
    'halt': 'Stop/shutdown a VM',
Noe Nieto's avatar
Noe Nieto committed
203
    'restart': 'Restarts the VM',
204 205 206 207 208 209
    'destroy': 'Destroy the VM and do cleanup',
    'ssh': 'Connect to the vm using SSH',
    'playbook': 'Run a playbook against a machine',
    'which': 'Print the path to the Vagrantfile',
    'open': 'Show the working directory of the VM using the file manager.'
}
Noe Nieto's avatar
Noe Nieto committed
210

Noe Nieto's avatar
Noe Nieto committed
211
if __name__ == '__main__':
212
    cmd_parser = argparse.ArgumentParser(
Noe Nieto's avatar
Noe Nieto committed
213
        prog='shipyard',
214
        description='This utility helps you mange virtual machines with Vagrant without dealing with configuration details',
Noe Nieto's avatar
Noe Nieto committed
215
    )
216 217
    cmd_parser.add_argument('command', choices=COMMANDS,)
    cmd_args = cmd_parser.parse_args(sys.argv[1:2])
Noe Nieto's avatar
Noe Nieto committed
218

Noe Nieto's avatar
Noe Nieto committed
219 220 221
    # Ensure XDG_CONFIG_HOME exists
    XDG_CONFIG_HOME.mkdir(parents=True, exist_ok=True)

222
    if cmd_args.command == 'status':
223
        print_status()
224 225 226 227 228 229 230 231 232 233 234 235 236 237
        sys.exit()

    sub_parser = argparse.ArgumentParser(prog='shipyard',)
    if cmd_args.command == 'create':
        sub_parser.usage = 'shipyard create <hostname> [--ram RAM] [--cpus CPUS]'
        sub_parser.add_argument('hostname', help='This is the hostname for the new the box')
        sub_parser.add_argument('--ram', help='Hoy much RAM for this box (default is 512)', default=512)
        sub_parser.add_argument('--cpus', help='Hoy many CPUs for this box (default is 1)', default=1)
        sub_args = sub_parser.parse_args(sys.argv[2:])
        create_box(sub_args.hostname, sub_args.ram, sub_args.cpus)
        sys.exit()

    sub_parser.add_argument('hostname', help='This is the hostname of the box')
    if cmd_args.command == 'playbook':
Noe Nieto's avatar
Noe Nieto committed
238 239
        sub_parser.usage = 'shipyard playbook hostname path/to/playbook.yml'
        sub_parser.add_argument('playbook', help='The path to the playbook file')
Noe Nieto's avatar
Noe Nieto committed
240
        sub_parser.add_argument('--retry', help='The path to the playbook retry file.', required=False)
241
        sub_args = sub_parser.parse_args(sys.argv[2:])
Noe Nieto's avatar
Noe Nieto committed
242
        run_playbook(sub_args.hostname, sub_args.playbook, sub_args.retry)
243 244 245 246 247
        sys.exit()

    sub_args = sub_parser.parse_args(sys.argv[2:])
    if cmd_args.command == 'up':
        start_vm(sub_args.hostname)
Noe Nieto's avatar
Noe Nieto committed
248 249
    if cmd_args.command == 'halt':
        halt_vm(sub_args.hostname)
Noe Nieto's avatar
Noe Nieto committed
250 251 252
    if cmd_args.command == 'restart':
        halt_vm(sub_args.hostname)
        start_vm(sub_args.hostname)
253
    elif cmd_args.command == 'destroy':
Noe Nieto's avatar
Noe Nieto committed
254
        destroy_vm(sub_args.hostname)
255 256 257 258 259 260
    elif cmd_args.command == 'ssh':
        invoke_shell(sub_args.hostname)
    elif cmd_args.command == 'which':
        which(sub_args.hostname)
    elif cmd_args.command == 'open':
        gio_open_vm(sub_args.hostname)
261
    else:
262
        cmd_parser.print_help()
263
        exit(100)