From 89ba1aad1e49c3e85dfe77789d6b19974341fe20 Mon Sep 17 00:00:00 2001
From: Karsten Jakobsen <kj@patientsky.com>
Date: Wed, 18 Mar 2020 08:59:16 +0100
Subject: [PATCH] Added custom scripts from other repo to running version

---
 docker-compose.yml    |   2 +-
 scripts/create_vms.py | 469 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 470 insertions(+), 1 deletion(-)
 create mode 100644 scripts/create_vms.py

diff --git a/docker-compose.yml b/docker-compose.yml
index 469202c..54232aa 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -39,7 +39,7 @@ services:
     - netbox-nginx-config:/etc/netbox-nginx/:ro
     - /etc/cert-client:/etc/cert-client:ro
   postgres:
-    image: postgres:11-alpine
+    image: postgres:10.4-alpine
     env_file: env/postgres.env
     volumes:
     - netbox-postgres-data:/var/lib/postgresql/data
diff --git a/scripts/create_vms.py b/scripts/create_vms.py
new file mode 100644
index 0000000..0a22557
--- /dev/null
+++ b/scripts/create_vms.py
@@ -0,0 +1,469 @@
+import netaddr
+
+from dcim.choices import InterfaceTypeChoices, InterfaceModeChoices
+from dcim.models import Platform, DeviceRole, Site
+from ipam.models import IPAddress, VRF, Interface, Prefix, VLAN
+from tenancy.models import Tenant
+from virtualization.models import VirtualMachine, Cluster
+from virtualization.choices import VirtualMachineStatusChoices
+from extras.scripts import Script, ObjectVar, ChoiceVar, TextVar, IntegerVar
+from extras.models import Tag
+from utilities.forms import APISelect
+
+
+class DeployVM(Script):
+
+    env = ""
+    interfaces = []
+    time_zone = ""
+    dns_domain_private = ""
+    dns_domain_public = ""
+    dns_servers = []
+    ntp_servers = []
+    ssh_authorized_keys = []
+    ssh_port = ""
+    _default_interface = {}
+    tags = []
+    output = []
+    success_log = ""
+
+    class Meta:
+        name = "Deploy new VMs"
+        description = "Deploy new virtual machines from existing platforms and roles using AWX"
+        fields = ['persist_disk', 'status', 'health_check', 'serial', 'tenant', 'cluster', 'env', 'untagged_vlan', 'backup', 'ip_addresses', 'vm_count', 'vcpus', 'memory', 'platform', 'role', 'disk', 'ssh_authorized_keys', 'hostnames']
+        field_order = ['status', 'tenant', 'cluster', 'env', 'platform', 'role', 'health_check', 'serial', 'persist_disk', 'backup', 'vm_count', 'vcpus', 'memory', 'disk', 'hostnames', 'untagged_vlan', 'ip_addresses', 'ssh_authorized_keys']
+        commit_default = False
+
+    health_check = ChoiceVar(
+        label="Health checks on deployment",
+        description="Deployment will fail if server does not pass Consul health checks",
+        required=True,
+        choices=(
+            ('True', 'Yes'),
+            ('False', 'No')
+        )
+    )
+
+    serial = ChoiceVar(
+        label="Serial deployment",
+        description="VM will not be parallelized in deployment",
+        required=True,
+        choices=(
+            ('False', 'No'),
+            ('True', 'Yes'),
+        )
+    )
+
+    persist_disk = ChoiceVar(
+        label="Persist volume on redeploy",
+        description="VM will persist disk2 in vSphere on redeploys",
+        required=True,
+        choices=(
+            ('False', 'No'),
+            ('True', 'Yes'),
+        )
+    )
+
+    status = ChoiceVar(
+        label="VM Status",
+        description="Deploy VM now or later?",
+        required=True,
+        choices=(
+            (VirtualMachineStatusChoices.STATUS_STAGED, 'Staged (Deploy now)'),
+            (VirtualMachineStatusChoices.STATUS_PLANNED, 'Planned (Save for later)')
+        )
+    )
+
+    tenant = ObjectVar(
+        default="patientsky-hosting",
+        description="Name of the tenant the VMs beloing to",
+        queryset=Tenant.objects.filter()
+    )
+
+    cluster = ObjectVar(
+        default="odn1",
+        description="Name of the vSphere cluster you are deploying to",
+        queryset=Cluster.objects.all()
+    )
+
+    env = ChoiceVar(
+        label="Environment",
+        description="Environment to deploy VM",
+        default="vlb",
+        choices=(
+            ('pno', 'pno'),
+            ('inf', 'inf'),
+            ('stg', 'stg'),
+            ('dev', 'dev'),
+            ('hem', 'hem'),
+            ('hov', 'hov'),
+            ('hpl', 'hpl'),
+            ('mgt', 'mgt'),
+            ('cse', 'cse'),
+            ('qua', 'qua'),
+            ('dmo', 'dmo'),
+            ('vlb', 'vlb'),
+            ('cmi', 'cmi')
+        )
+    )
+
+    platform = ObjectVar(
+        description="Host OS to deploy",
+        queryset=Platform.objects.filter(
+            name__regex=r'^(base_.*)'
+        ).order_by('name')
+    )
+
+    role = ObjectVar(
+        label="VM Role",
+        description="VM Role",
+        queryset=DeviceRole.objects.filter(
+            vm_role=True
+        ).order_by('name')
+    )
+
+    backup = ChoiceVar(
+        label="Backup strategy",
+        description="The backup strategy deployed to this VM with Veeam",
+        required=True,
+        choices=(
+            ('nobackup', 'Never'),
+            ('backup_general_1', 'Daily'),
+            ('backup_general_4', 'Monthly')
+        )
+    )
+
+    ip_addresses = TextVar(
+        required=False,
+        label="IP Addresses",
+        description="List of IP addresses to create w. prefix e.g 192.168.0.10/24. If none given, hosts will be assigned IPs 'automagically'"
+    )
+
+    untagged_vlan = ObjectVar(
+        required=False,
+        label="VLAN",
+        widget=APISelect(api_url='/api/ipam/vlans/', display_field='display_name'),
+        queryset=VLAN.objects.all(),
+        description="Choose VLAN for IP-addresses",
+    )
+
+    hostnames = TextVar(
+        required=True,
+        label="Hostnames",
+        description="List of hostnames to create."
+    )
+
+    ssh_authorized_keys = TextVar(
+        label="SSH Authorized Keys",
+        required=False,
+        description="List of accepted SSH keys - defaults to site config context 'ssh_authorized_keys'"
+    )
+
+    vm_count = ChoiceVar(
+        label="Number of VMs",
+        description="Number of VMs to deploy",
+        choices=(
+            ('1', '1'),
+            ('2', '2'),
+            ('3', '3'),
+            ('4', '4'),
+            ('5', '5'),
+            ('6', '6'),
+            ('7', '7'),
+            ('8', '8'),
+            ('9', '9'),
+            ('10', '10')
+        )
+    )
+
+    vcpus = ChoiceVar(
+        label="Number of CPUs",
+        description="Number of virtual CPUs",
+        default="2",
+        choices=(
+            ('1', '1'),
+            ('2', '2'),
+            ('3', '3'),
+            ('4', '4'),
+            ('5', '5'),
+            ('6', '6'),
+            ('7', '7'),
+            ('8', '8')
+        )
+    )
+
+    memory = ChoiceVar(
+        description="Amount of VM memory",
+        default="4096",
+        choices=(
+            ('1024', '1024'),
+            ('2048', '2048'),
+            ('4096', '4096'),
+            ('8192', '8192'),
+            ('16384', '16384'),
+            ('32768', '32768')
+        )
+    )
+
+    disk = IntegerVar(
+        label="Disk size",
+        description="Disk size in GB",
+        default="20"
+    )
+
+    def appendLogSuccess(self, log: str, obj=None):
+        self.success_log += " {} `\n{}\n`".format(log, obj)
+        return self
+
+    def flushLogSuccess(self):
+        self.log_success(self.success_log)
+        self.success_log = ""
+        return self
+
+    def _generateHostname(self, cluster, env, descriptor):
+
+        # I now proclaim this VM, First of its Name, Queen of the Andals and the First Men, Protector of the Seven Kingdoms
+        vm_index = "001"
+
+        search_for = str(cluster) + '-' + env + '-' + descriptor + '-'
+        vms = VirtualMachine.objects.filter(
+            name__startswith=search_for
+        )
+
+        if len(vms) > 0:
+            # Get last of its kind
+            last_vm_index = int(vms[len(vms) - 1].name.split('-')[3]) + 1
+            if last_vm_index < 10:
+                vm_index = '00' + str(last_vm_index)
+            elif last_vm_index < 100:
+                vm_index = '0' + str(last_vm_index)
+            else:
+                vm_index = str(last_vm_index)
+
+        hostname = str(cluster) + '-' + env + '-' + descriptor + '-' + vm_index
+
+        return hostname
+
+    def __validateInput(self, data, base_context_data):
+
+        try:
+            self.interfaces = base_context_data['interfaces']
+            self.time_zone = base_context_data['time_zone']
+            self.tags = base_context_data['tags']
+            self.dns_domain_private = base_context_data['dns_domain_private']
+            self.dns_domain_public = base_context_data['dns_domain_public']
+            self.dns_servers = base_context_data['dns_servers']
+            self.ntp_servers = base_context_data['ntp_servers']
+            self.ssh_authorized_keys = base_context_data['ssh_authorized_keys']
+            self.ssh_port = base_context_data['ssh_port']
+            self.tags.update({'env_' + self.env: {'comments': 'Environment', 'color': '009688'}})
+            self.tags.update({'vsphere_' + data['backup']: {'comments': 'Backup strategy', 'color': '009688'}})
+            if (data['health_check'] == 'True'):
+                self.tags.update({'health_check': {'comments': 'Do health checks in deployment', 'color': '4caf50'}})
+            if (data['serial'] == 'True'):
+                self.tags.update({'serial': {'comments': 'Do health checks in deployment', 'color': '4caf50'}})
+        except Exception as error:
+            self.log_failure("Error when parsing context_data! Error: " + str(error))
+            return False
+
+        if self.interfaces is None:
+            self.log_failure("No interfaces object in context data!")
+            return False
+
+        if data['ip_addresses'] != "" and len(data['ip_addresses'].splitlines()) != int(data['vm_count']):
+            self.log_failure("The number of IP addresses and VMs does not match!")
+            return False
+
+        if data['hostnames'] != "" and len(data['hostnames'].splitlines()) != int(data['vm_count']):
+            self.log_failure("The number of hostnames and VMs does not match!")
+            return False
+
+        if self.interfaces is None:
+            self.log_failure("No interfaces object in context data!")
+            return False
+
+        return True
+
+    def run(self, data):
+
+        vrf = VRF.objects.get(
+            name="global"
+        )
+
+        self.env = data['env']
+
+        # Setup base virtual machine for copying config_context
+        base_vm = VirtualMachine(
+            cluster=data['cluster'],
+            platform=data['platform'],
+            role=data['role'],
+            tenant=data['tenant'],
+            name="script_temp"
+        )
+
+        base_vm.save()
+
+        if self.__validateInput(data=data, base_context_data=base_vm.get_config_context()) is False:
+            return False
+
+        # Delete base virtual machine for copying config_context
+        vm_delete = VirtualMachine.objects.get(
+            name="script_temp"
+        )
+        vm_delete.delete()
+
+        for i in range(0, int(data['vm_count'])):
+
+            hostnames = data['hostnames'].splitlines()
+            ip_addresses = data['ip_addresses'].splitlines()
+
+            if len(hostnames) > 0:
+                hostname = hostnames[i]
+            else:
+                hostname = self._generateHostname(
+                    cluster=data['cluster'].name,
+                    env=self.env,
+                    descriptor="na"
+                )
+
+            # Check if VM exists
+            if len(VirtualMachine.objects.filter(name=hostname)) > 0:
+                self.log_failure("VM with hostname " + hostname + " already exists!")
+                return False
+
+            if len(ip_addresses) > 0:
+
+                # Check if IP address exists
+                ip_check = IPAddress.objects.filter(
+                    address=ip_addresses[i]
+                )
+
+                if len(ip_check) > 0:
+                    self.log_failure(str(ip_check[0].address) + ' is already assigned to ' + str(ip_check[0].interface.name))
+                    return False
+
+                domain = self.dns_domain_private if netaddr.IPNetwork(ip_addresses[i]).is_private() is True else self.dns_domain_public
+                ip_address = IPAddress(
+                    address=ip_addresses[i],
+                    vrf=vrf,
+                    tenant=data['tenant'],
+                    family=4,
+                    dns_name=hostname + '.' + domain,
+                )
+
+                ip_address.save()
+                self.appendLogSuccess(log="Created IP", obj=ip_addresses[i]).appendLogSuccess(log="DNS", obj=hostname + '.' + domain)
+            else:
+                if data['untagged_vlan'] is not None:
+
+                    try:
+
+                        # Auto assign IPs from vsphere_port_group
+                        prefix = Prefix.objects.get(
+                            vlan=data['untagged_vlan'],
+                            site=Site.objects.get(name=data['cluster'].site.name),
+                            is_pool=True
+                        )
+
+                        ip = prefix.get_first_available_ip()
+
+                        domain = self.dns_domain_private if netaddr.IPNetwork(ip).is_private() is True else self.dns_domain_public
+                        ip_address = IPAddress(
+                            address=ip,
+                            vrf=vrf,
+                            tenant=data['tenant'],
+                            family=4,
+                            dns_name=hostname + '.' + domain,
+                        )
+
+                        ip_address.save()
+                        self.appendLogSuccess(log="Auto-assigned IP", obj=ip).appendLogSuccess(log="DNS", obj=hostname + '.' + domain)
+                    except Exception:
+                        self.log_failure("An error occurred while auto-assigning IP address. VLAN or Prefix not found!")
+                        return False
+                else:
+                    self.log_failure("No IP address choice was made")
+                    return False
+
+            vm = VirtualMachine(
+                status=data['status'],
+                cluster=data['cluster'],
+                platform=data['platform'],
+                role=data['role'],
+                tenant=data['tenant'],
+                name=hostname,
+                disk=data['disk'],
+                memory=data['memory'],
+                vcpus=data['vcpus']
+            )
+
+            vm.primary_ip4 = ip_address
+            vm.save()
+            self.appendLogSuccess(log="for VM in ", obj=vm.cluster)
+
+            self.__assignTags(vm=vm)
+
+            # Assign IP to interface
+            if self.__setupInterface(ip_address=ip_address, data=data, vm=vm) is False:
+                return False
+
+            self.flushLogSuccess()
+
+        return self.success_log
+
+    def __assignTags(self, vm: VirtualMachine):
+        """
+        Assign tags from context data
+        """
+        for tag in self.tags:
+            if len(Tag.objects.filter(name=tag)) == 0:
+                color = self.tags[tag]['color'] if 'color' in self.tags[tag] else '9e9e9e'
+                comments = self.tags[tag]['comments'] if 'comments' in self.tags[tag] else 'No comments'
+                newTag = Tag(comments=comments, name=tag, color=color)
+                newTag.save()
+
+            vm.tags.add(tag)
+
+        vm.save()
+
+    def __setupInterface(self, ip_address: IPAddress, vm: VirtualMachine, data):
+        """
+        Setup interface and add IP address
+        """
+
+        # Get net address tools
+        ip = netaddr.IPNetwork(ip_address.address)
+        prefix_search = str(ip.network) + '/' + str(ip.prefixlen)
+
+        try:
+            prefix = Prefix.objects.get(
+                prefix=prefix_search,
+                is_pool=True
+            )
+        except Exception:
+            self.log_failure("Prefix for IP " + ip_address.address + " was not found")
+            return False
+
+        # Right now we dont support multiple nics. But the data model supports it
+        interface = Interface(
+            name=self.interfaces['nic0']['name'],
+            mtu=self.interfaces['nic0']['mtu'],
+            virtual_machine=vm,
+            type=InterfaceTypeChoices.TYPE_VIRTUAL
+        )
+
+        # If we need anything other than Access, here is were to change it
+        if self.interfaces['nic0']['mode'] == "Access":
+            interface.mode = InterfaceModeChoices.MODE_ACCESS
+            interface.untagged_vlan = prefix.vlan
+            self.interfaces['nic0']['vsphere_port_group'] = prefix.vlan.name
+
+        interface.save()
+
+        # Add interface to IP address
+        ip_address.interface = interface
+        ip_address.save()
+
+        self.appendLogSuccess(log="with interface ", obj=interface).appendLogSuccess(log=", vlan ", obj=prefix.vlan.name)
+
+        return True