From 0e73d829e8b60101bd3826349436a1599a8e4476 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Tue, 19 Nov 2019 11:56:15 +0000 Subject: [PATCH 01/21] Add custom script for creating VM --- scripts/create_vm.py | 101 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 scripts/create_vm.py diff --git a/scripts/create_vm.py b/scripts/create_vm.py new file mode 100644 index 0000000..dde797c --- /dev/null +++ b/scripts/create_vm.py @@ -0,0 +1,101 @@ +""" +This script allows you to create a VM, an interface and primary IP address +all in one screen. + +Workaround for issues: +https://github.com/netbox-community/netbox/issues/1492 +https://github.com/netbox-community/netbox/issues/648 +""" + +from dcim.constants import DEVICE_STATUS_ACTIVE, IFACE_TYPE_VIRTUAL +from dcim.models import DeviceRole, Platform, Interface +from django.core.exceptions import ObjectDoesNotExist +from ipam.constants import IPADDRESS_STATUS_ACTIVE +from ipam.models import IPAddress, VRF +from tenancy.models import Tenant +from virtualization.constants import VM_STATUS_CHOICES +from virtualization.models import Cluster, VirtualMachine +from extras.scripts import Script, StringVar, IPNetworkVar, ObjectVar, ChoiceVar, IntegerVar, TextVar + +class NewVM(Script): + class Meta: + name = "New VM" + description = "Create a new VM" + field_order = ['vm_name', 'dns_name', 'primary_ip4', 'primary_ip6', #'vrf', + 'role', 'status', 'cluster', #'tenant', + 'platform', 'interface_name', 'mac_address', + 'vcpus', 'memory', 'disk', 'comments'] + + vm_name = StringVar(label="VM name") + dns_name = StringVar(label="DNS name", required=False) + primary_ip4 = IPNetworkVar(label="IPv4 address") + primary_ip6 = IPNetworkVar(label="IPv6 address", required=False) + #vrf = ObjectVar(VRF.objects, required=False) + role = ObjectVar(DeviceRole.objects.filter(vm_role=True)) + status = ChoiceVar(VM_STATUS_CHOICES, default=DEVICE_STATUS_ACTIVE) + cluster = ObjectVar(Cluster.objects) + #tenant = ObjectVar(Tenant.objects, required=False) + platform = ObjectVar(Platform.objects, required=False) + interface_name = StringVar(default="eth0") + mac_address = StringVar(label="MAC address", required=False) + vcpus = IntegerVar(label="VCPUs", required=False) + memory = IntegerVar(label="Memory (MB)", required=False) + disk = IntegerVar(label="Disk (GB)", required=False) + comments = TextVar(label="Comments", required=False) + + def run(self, data): + vm = VirtualMachine( + name=data["vm_name"], + role=data["role"], + status=data["status"], + cluster=data["cluster"], + platform=data["platform"], + vcpus=data["vcpus"], + memory=data["memory"], + disk=data["disk"], + comments=data["comments"], + tenant=data.get("tenant"), + ) + vm.save() + + interface = Interface( + name=data["interface_name"], + type=IFACE_TYPE_VIRTUAL, + mac_address=data["mac_address"], + virtual_machine=vm, + ) + interface.save() + + def add_addr(addr, expect_family): + if not addr: + return + if addr.version != expect_family: + raise RuntimeError("Wrong family for %r" % a) + try: + a = IPAddress.objects.get( + address=addr, + family=addr.version, + vrf=data.get("vrf"), + ) + result = "Assigned" + except ObjectDoesNotExist: + a = IPAddress( + address=addr, + family=addr.version, + vrf=data.get("vrf"), + ) + result = "Created" + a.status = IPADDRESS_STATUS_ACTIVE + a.dns_name = data["dns_name"] + if a.interface: + raise RuntimeError("Address %s is already assigned" % addr) + a.interface = interface + a.tenant = data.get("tenant") + a.save() + self.log_info("%s IP address %s %s" % (result, a.address, a.vrf or "")) + setattr(vm, "primary_ip%d" % a.family, a) + + add_addr(data["primary_ip4"], 4) + add_addr(data["primary_ip6"], 6) + vm.save() + self.log_success("Created VM %s" % vm.name) From 610742084b6baa877322c513f67c6e83c19f6987 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Sun, 23 Feb 2020 13:51:45 +0000 Subject: [PATCH 02/21] Fixes for v2.7: * New values for choices * Use IPAddressWithMaskVar * Make VM role optional --- scripts/create_vm.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index dde797c..e5dc78d 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -7,15 +7,15 @@ https://github.com/netbox-community/netbox/issues/648 """ -from dcim.constants import DEVICE_STATUS_ACTIVE, IFACE_TYPE_VIRTUAL +from dcim.choices import InterfaceTypeChoices from dcim.models import DeviceRole, Platform, Interface from django.core.exceptions import ObjectDoesNotExist -from ipam.constants import IPADDRESS_STATUS_ACTIVE +from ipam.choices import IPAddressStatusChoices from ipam.models import IPAddress, VRF from tenancy.models import Tenant -from virtualization.constants import VM_STATUS_CHOICES +from virtualization.choices import VirtualMachineStatusChoices from virtualization.models import Cluster, VirtualMachine -from extras.scripts import Script, StringVar, IPNetworkVar, ObjectVar, ChoiceVar, IntegerVar, TextVar +from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar, ChoiceVar, IntegerVar, TextVar class NewVM(Script): class Meta: @@ -28,11 +28,11 @@ class Meta: vm_name = StringVar(label="VM name") dns_name = StringVar(label="DNS name", required=False) - primary_ip4 = IPNetworkVar(label="IPv4 address") - primary_ip6 = IPNetworkVar(label="IPv6 address", required=False) + primary_ip4 = IPAddressWithMaskVar(label="IPv4 address") + primary_ip6 = IPAddressWithMaskVar(label="IPv6 address", required=False) #vrf = ObjectVar(VRF.objects, required=False) - role = ObjectVar(DeviceRole.objects.filter(vm_role=True)) - status = ChoiceVar(VM_STATUS_CHOICES, default=DEVICE_STATUS_ACTIVE) + role = ObjectVar(DeviceRole.objects.filter(vm_role=True), required=False) + status = ChoiceVar(VirtualMachineStatusChoices, default=VirtualMachineStatusChoices.STATUS_ACTIVE) cluster = ObjectVar(Cluster.objects) #tenant = ObjectVar(Tenant.objects, required=False) platform = ObjectVar(Platform.objects, required=False) @@ -60,7 +60,7 @@ def run(self, data): interface = Interface( name=data["interface_name"], - type=IFACE_TYPE_VIRTUAL, + type=InterfaceTypeChoices.TYPE_VIRTUAL, mac_address=data["mac_address"], virtual_machine=vm, ) @@ -85,7 +85,7 @@ def add_addr(addr, expect_family): vrf=data.get("vrf"), ) result = "Created" - a.status = IPADDRESS_STATUS_ACTIVE + a.status = IPAddressStatusChoices.STATUS_ACTIVE a.dns_name = data["dns_name"] if a.interface: raise RuntimeError("Address %s is already assigned" % addr) From c3d2dbdd8f5de2bf58447caa4ad3938e43c746e7 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Fri, 5 Jun 2020 10:38:09 +0100 Subject: [PATCH 03/21] Remove IPAddress.family which was removed from Netbox data model in v2.8.0 Fixes #29 --- scripts/create_vm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index e5dc78d..06cbfaf 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -74,14 +74,12 @@ def add_addr(addr, expect_family): try: a = IPAddress.objects.get( address=addr, - family=addr.version, vrf=data.get("vrf"), ) result = "Assigned" except ObjectDoesNotExist: a = IPAddress( address=addr, - family=addr.version, vrf=data.get("vrf"), ) result = "Created" From a9ae7273cfa3a4ab4cb9f801f0af818a1a79d939 Mon Sep 17 00:00:00 2001 From: Adrian Kus <47573296+adriankus@users.noreply.github.com> Date: Tue, 20 Oct 2020 11:13:23 +0100 Subject: [PATCH 04/21] Update create_vm.py (#41) Updated for Netbox 2.9 --- scripts/create_vm.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 06cbfaf..c6f22f1 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -1,7 +1,6 @@ """ This script allows you to create a VM, an interface and primary IP address all in one screen. - Workaround for issues: https://github.com/netbox-community/netbox/issues/1492 https://github.com/netbox-community/netbox/issues/648 @@ -14,8 +13,8 @@ from ipam.models import IPAddress, VRF from tenancy.models import Tenant from virtualization.choices import VirtualMachineStatusChoices -from virtualization.models import Cluster, VirtualMachine -from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar, ChoiceVar, IntegerVar, TextVar +from virtualization.models import Cluster, VirtualMachine, VMInterface +from extras.scripts import Script, StringVar, IPAddressWithMaskVar, MultiObjectVar, ObjectVar, ChoiceVar, IntegerVar, TextVar class NewVM(Script): class Meta: @@ -30,12 +29,12 @@ class Meta: dns_name = StringVar(label="DNS name", required=False) primary_ip4 = IPAddressWithMaskVar(label="IPv4 address") primary_ip6 = IPAddressWithMaskVar(label="IPv6 address", required=False) - #vrf = ObjectVar(VRF.objects, required=False) - role = ObjectVar(DeviceRole.objects.filter(vm_role=True), required=False) + #vrf = ObjectVar(model=VRF, required=False) + role = ObjectVar(model=DeviceRole, query_params={'vm_role':'True'}) status = ChoiceVar(VirtualMachineStatusChoices, default=VirtualMachineStatusChoices.STATUS_ACTIVE) - cluster = ObjectVar(Cluster.objects) - #tenant = ObjectVar(Tenant.objects, required=False) - platform = ObjectVar(Platform.objects, required=False) + cluster = ObjectVar(model=Cluster) + tenant = ObjectVar(model=Tenant, required=False) + platform = ObjectVar(model=Platform) interface_name = StringVar(default="eth0") mac_address = StringVar(label="MAC address", required=False) vcpus = IntegerVar(label="VCPUs", required=False) @@ -58,9 +57,8 @@ def run(self, data): ) vm.save() - interface = Interface( + interface = VMInterface( name=data["interface_name"], - type=InterfaceTypeChoices.TYPE_VIRTUAL, mac_address=data["mac_address"], virtual_machine=vm, ) @@ -85,9 +83,9 @@ def add_addr(addr, expect_family): result = "Created" a.status = IPAddressStatusChoices.STATUS_ACTIVE a.dns_name = data["dns_name"] - if a.interface: + if result == "Assigned": raise RuntimeError("Address %s is already assigned" % addr) - a.interface = interface + a.assigned_object = interface a.tenant = data.get("tenant") a.save() self.log_info("%s IP address %s %s" % (result, a.address, a.vrf or "")) @@ -97,3 +95,4 @@ def add_addr(addr, expect_family): add_addr(data["primary_ip6"], 6) vm.save() self.log_success("Created VM %s" % vm.name) + From ba1b325d80830535c1e4cf9ea2fdb37c7be64b78 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Tue, 20 Oct 2020 23:37:19 +0100 Subject: [PATCH 05/21] Minor updates to scripts/create_vm.py (#39) --- scripts/create_vm.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index c6f22f1..4c5a201 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -1,20 +1,20 @@ """ This script allows you to create a VM, an interface and primary IP address all in one screen. + Workaround for issues: https://github.com/netbox-community/netbox/issues/1492 https://github.com/netbox-community/netbox/issues/648 """ -from dcim.choices import InterfaceTypeChoices -from dcim.models import DeviceRole, Platform, Interface +from dcim.models import DeviceRole, Platform from django.core.exceptions import ObjectDoesNotExist from ipam.choices import IPAddressStatusChoices from ipam.models import IPAddress, VRF from tenancy.models import Tenant from virtualization.choices import VirtualMachineStatusChoices from virtualization.models import Cluster, VirtualMachine, VMInterface -from extras.scripts import Script, StringVar, IPAddressWithMaskVar, MultiObjectVar, ObjectVar, ChoiceVar, IntegerVar, TextVar +from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar, ChoiceVar, IntegerVar, TextVar class NewVM(Script): class Meta: @@ -30,11 +30,11 @@ class Meta: primary_ip4 = IPAddressWithMaskVar(label="IPv4 address") primary_ip6 = IPAddressWithMaskVar(label="IPv6 address", required=False) #vrf = ObjectVar(model=VRF, required=False) - role = ObjectVar(model=DeviceRole, query_params={'vm_role':'True'}) + role = ObjectVar(model=DeviceRole, query_params=dict(vm_role=True), required=False) status = ChoiceVar(VirtualMachineStatusChoices, default=VirtualMachineStatusChoices.STATUS_ACTIVE) cluster = ObjectVar(model=Cluster) tenant = ObjectVar(model=Tenant, required=False) - platform = ObjectVar(model=Platform) + platform = ObjectVar(model=Platform, required=False) interface_name = StringVar(default="eth0") mac_address = StringVar(label="MAC address", required=False) vcpus = IntegerVar(label="VCPUs", required=False) @@ -57,12 +57,12 @@ def run(self, data): ) vm.save() - interface = VMInterface( + vminterface = VMInterface( name=data["interface_name"], mac_address=data["mac_address"], virtual_machine=vm, ) - interface.save() + vminterface.save() def add_addr(addr, expect_family): if not addr: @@ -83,9 +83,9 @@ def add_addr(addr, expect_family): result = "Created" a.status = IPAddressStatusChoices.STATUS_ACTIVE a.dns_name = data["dns_name"] - if result == "Assigned": + if a.assigned_object: raise RuntimeError("Address %s is already assigned" % addr) - a.assigned_object = interface + a.assigned_object = vminterface a.tenant = data.get("tenant") a.save() self.log_info("%s IP address %s %s" % (result, a.address, a.vrf or "")) @@ -95,4 +95,3 @@ def add_addr(addr, expect_family): add_addr(data["primary_ip6"], 6) vm.save() self.log_success("Created VM %s" % vm.name) - From 74b89f0c2e6b848b9c70789060ac3cc3cdf477df Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Thu, 18 Feb 2021 22:44:15 +0000 Subject: [PATCH 06/21] Netbox 2.10 preparation (#43) Custom script run() method must take a 'commit' argument --- scripts/create_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 4c5a201..8ea10de 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -42,7 +42,7 @@ class Meta: disk = IntegerVar(label="Disk (GB)", required=False) comments = TextVar(label="Comments", required=False) - def run(self, data): + def run(self, data, commit): vm = VirtualMachine( name=data["vm_name"], role=data["role"], From c6152bc6d78007fbc700cf3c0ecfafcd49746057 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Tue, 11 Jan 2022 12:09:22 +0000 Subject: [PATCH 07/21] Add object.full_clean() when creating objects Otherwise it's possible to create objects which bypass the ORM validation rules. --- scripts/create_vm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 8ea10de..83c9416 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -21,7 +21,7 @@ class Meta: name = "New VM" description = "Create a new VM" field_order = ['vm_name', 'dns_name', 'primary_ip4', 'primary_ip6', #'vrf', - 'role', 'status', 'cluster', #'tenant', + 'role', 'status', 'cluster', 'tenant', 'platform', 'interface_name', 'mac_address', 'vcpus', 'memory', 'disk', 'comments'] @@ -55,6 +55,7 @@ def run(self, data, commit): comments=data["comments"], tenant=data.get("tenant"), ) + vm.full_clean() vm.save() vminterface = VMInterface( @@ -62,6 +63,7 @@ def run(self, data, commit): mac_address=data["mac_address"], virtual_machine=vm, ) + vminterface.full_clean() vminterface.save() def add_addr(addr, expect_family): @@ -87,11 +89,13 @@ def add_addr(addr, expect_family): raise RuntimeError("Address %s is already assigned" % addr) a.assigned_object = vminterface a.tenant = data.get("tenant") + a.full_clean() a.save() self.log_info("%s IP address %s %s" % (result, a.address, a.vrf or "")) setattr(vm, "primary_ip%d" % a.family, a) add_addr(data["primary_ip4"], 4) add_addr(data["primary_ip6"], 6) + vm.full_clean() vm.save() self.log_success("Created VM %s" % vm.name) From f53db699db77f408f8eadb1057e370e9354cb0f7 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Wed, 2 Feb 2022 10:16:28 +0000 Subject: [PATCH 08/21] Correct way of assigning tags to objects Also use f"..." formatting and remove unnecessary field_order --- scripts/create_vm.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 83c9416..8d77cea 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -9,26 +9,26 @@ from dcim.models import DeviceRole, Platform from django.core.exceptions import ObjectDoesNotExist +from extras.models import Tag from ipam.choices import IPAddressStatusChoices from ipam.models import IPAddress, VRF from tenancy.models import Tenant from virtualization.choices import VirtualMachineStatusChoices from virtualization.models import Cluster, VirtualMachine, VMInterface -from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar, ChoiceVar, IntegerVar, TextVar +from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar, MultiObjectVar, ChoiceVar, IntegerVar, TextVar class NewVM(Script): class Meta: name = "New VM" description = "Create a new VM" - field_order = ['vm_name', 'dns_name', 'primary_ip4', 'primary_ip6', #'vrf', - 'role', 'status', 'cluster', 'tenant', - 'platform', 'interface_name', 'mac_address', - 'vcpus', 'memory', 'disk', 'comments'] vm_name = StringVar(label="VM name") dns_name = StringVar(label="DNS name", required=False) + vm_tags = MultiObjectVar(model=Tag, label="VM tags", required=False) primary_ip4 = IPAddressWithMaskVar(label="IPv4 address") + #primary_ip4_tags = MultiObjectVar(model=Tag, label="IPv4 tags", required=False) primary_ip6 = IPAddressWithMaskVar(label="IPv6 address", required=False) + #primary_ip6_tags = MultiObjectVar(model=Tag, label="IPv6 tags", required=False) #vrf = ObjectVar(model=VRF, required=False) role = ObjectVar(model=DeviceRole, query_params=dict(vm_role=True), required=False) status = ChoiceVar(VirtualMachineStatusChoices, default=VirtualMachineStatusChoices.STATUS_ACTIVE) @@ -57,6 +57,7 @@ def run(self, data, commit): ) vm.full_clean() vm.save() + vm.tags.set(data["vm_tags"]) vminterface = VMInterface( name=data["interface_name"], @@ -66,11 +67,11 @@ def run(self, data, commit): vminterface.full_clean() vminterface.save() - def add_addr(addr, expect_family): + def add_addr(addr, family): if not addr: return - if addr.version != expect_family: - raise RuntimeError("Wrong family for %r" % a) + if addr.version != family: + raise RuntimeError(f"Wrong family for {a}") try: a = IPAddress.objects.get( address=addr, @@ -86,16 +87,17 @@ def add_addr(addr, expect_family): a.status = IPAddressStatusChoices.STATUS_ACTIVE a.dns_name = data["dns_name"] if a.assigned_object: - raise RuntimeError("Address %s is already assigned" % addr) + raise RuntimeError(f"Address {addr} is already assigned") a.assigned_object = vminterface a.tenant = data.get("tenant") a.full_clean() a.save() - self.log_info("%s IP address %s %s" % (result, a.address, a.vrf or "")) - setattr(vm, "primary_ip%d" % a.family, a) + #a.tags.set(data[f"primary_ip{family}_tags"]) + self.log_info(f"{result} IP address {a.address} {a.vrf or ''}") + setattr(vm, f"primary_ip{family}", a) add_addr(data["primary_ip4"], 4) add_addr(data["primary_ip6"], 6) vm.full_clean() vm.save() - self.log_success("Created VM %s" % vm.name) + self.log_success(f"Created VM [{vm.name}](/virtualization/virtual-machines/{vm.id}/)") From ddd1fb7bcbbff128319031da9ab1fc9d32564af3 Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 22 Dec 2022 14:30:24 +0000 Subject: [PATCH 09/21] Always run pylint against all python files --- .github/workflows/pylint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 0803911..e34a509 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -23,4 +23,4 @@ jobs: pip install -r requirements.txt - name: Analysing any changed code with pylint run: | - git diff --name-only HEAD^ | grep '^aquilon/' | xargs -r pylint --max-line-length=120 + find . -type f -name '*.py' | xargs -r pylint --max-line-length=120 From 3bb80ca776150c6bb8f85b0c5566ebc34edc6e9a Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:49:19 +0000 Subject: [PATCH 10/21] Disable irrelevant pylint warnings --- scripts/create_vm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 8d77cea..4db3160 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -7,6 +7,8 @@ https://github.com/netbox-community/netbox/issues/648 """ +# pylint: disable=missing-function-docstring,too-few-public-methods,import-error,missing-class-docstring + from dcim.models import DeviceRole, Platform from django.core.exceptions import ObjectDoesNotExist from extras.models import Tag From 5e5a95d0c01879fbcf06792f3789daaf0231b7d0 Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:22:04 +0000 Subject: [PATCH 11/21] Remove commented out lines Also remove references to values set by commented out lines. --- scripts/create_vm.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 4db3160..308a403 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -28,10 +28,7 @@ class Meta: dns_name = StringVar(label="DNS name", required=False) vm_tags = MultiObjectVar(model=Tag, label="VM tags", required=False) primary_ip4 = IPAddressWithMaskVar(label="IPv4 address") - #primary_ip4_tags = MultiObjectVar(model=Tag, label="IPv4 tags", required=False) primary_ip6 = IPAddressWithMaskVar(label="IPv6 address", required=False) - #primary_ip6_tags = MultiObjectVar(model=Tag, label="IPv6 tags", required=False) - #vrf = ObjectVar(model=VRF, required=False) role = ObjectVar(model=DeviceRole, query_params=dict(vm_role=True), required=False) status = ChoiceVar(VirtualMachineStatusChoices, default=VirtualMachineStatusChoices.STATUS_ACTIVE) cluster = ObjectVar(model=Cluster) @@ -77,13 +74,11 @@ def add_addr(addr, family): try: a = IPAddress.objects.get( address=addr, - vrf=data.get("vrf"), ) result = "Assigned" except ObjectDoesNotExist: a = IPAddress( address=addr, - vrf=data.get("vrf"), ) result = "Created" a.status = IPAddressStatusChoices.STATUS_ACTIVE @@ -94,7 +89,6 @@ def add_addr(addr, family): a.tenant = data.get("tenant") a.full_clean() a.save() - #a.tags.set(data[f"primary_ip{family}_tags"]) self.log_info(f"{result} IP address {a.address} {a.vrf or ''}") setattr(vm, f"primary_ip{family}", a) From de94d504c926d051d515bc864710e3a030aba9f5 Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:22:21 +0000 Subject: [PATCH 12/21] Fix long import line pep-0328 --- scripts/create_vm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 308a403..a1360d4 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -12,12 +12,14 @@ from dcim.models import DeviceRole, Platform from django.core.exceptions import ObjectDoesNotExist from extras.models import Tag +from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar +from extras.scripts import MultiObjectVar, ChoiceVar, IntegerVar, TextVar from ipam.choices import IPAddressStatusChoices from ipam.models import IPAddress, VRF from tenancy.models import Tenant from virtualization.choices import VirtualMachineStatusChoices from virtualization.models import Cluster, VirtualMachine, VMInterface -from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar, MultiObjectVar, ChoiceVar, IntegerVar, TextVar + class NewVM(Script): class Meta: From 28f03829883d2a6c454444f9bf9f99021f558944 Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:50:00 +0000 Subject: [PATCH 13/21] Remove unused import --- scripts/create_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index a1360d4..fd0456e 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -15,7 +15,7 @@ from extras.scripts import Script, StringVar, IPAddressWithMaskVar, ObjectVar from extras.scripts import MultiObjectVar, ChoiceVar, IntegerVar, TextVar from ipam.choices import IPAddressStatusChoices -from ipam.models import IPAddress, VRF +from ipam.models import IPAddress from tenancy.models import Tenant from virtualization.choices import VirtualMachineStatusChoices from virtualization.models import Cluster, VirtualMachine, VMInterface From a403c4d4b3058af005863880594a751c9bdaf985 Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:23:07 +0000 Subject: [PATCH 14/21] Fix reference to incorrect variable --- scripts/create_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index fd0456e..921f06a 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -72,7 +72,7 @@ def add_addr(addr, family): if not addr: return if addr.version != family: - raise RuntimeError(f"Wrong family for {a}") + raise RuntimeError(f"Wrong family for {addr}") try: a = IPAddress.objects.get( address=addr, From cf164c2b9d72cc0f6dbd6f9422c977339d66562c Mon Sep 17 00:00:00 2001 From: James Adams Date: Fri, 20 Jan 2023 08:22:12 +0000 Subject: [PATCH 15/21] pylint: Ignore unused argument to run() This gets passed as a keyword argument by the script framework, so has to be defined. --- scripts/create_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 921f06a..a0a60f2 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -43,7 +43,7 @@ class Meta: disk = IntegerVar(label="Disk (GB)", required=False) comments = TextVar(label="Comments", required=False) - def run(self, data, commit): + def run(self, data, commit): # pylint: disable=unused-argument vm = VirtualMachine( name=data["vm_name"], role=data["role"], From 91be9fda372fb8335feea8ed9eacf2206ac78367 Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:33:08 +0000 Subject: [PATCH 16/21] Use PEP-8 style name for virtual_machine --- scripts/create_vm.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index a0a60f2..ee4f9a9 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -44,7 +44,7 @@ class Meta: comments = TextVar(label="Comments", required=False) def run(self, data, commit): # pylint: disable=unused-argument - vm = VirtualMachine( + virtual_machine = VirtualMachine( name=data["vm_name"], role=data["role"], status=data["status"], @@ -56,14 +56,14 @@ def run(self, data, commit): # pylint: disable=unused-argument comments=data["comments"], tenant=data.get("tenant"), ) - vm.full_clean() - vm.save() - vm.tags.set(data["vm_tags"]) + virtual_machine.full_clean() + virtual_machine.save() + virtual_machine.tags.set(data["vm_tags"]) vminterface = VMInterface( name=data["interface_name"], mac_address=data["mac_address"], - virtual_machine=vm, + virtual_machine=virtual_machine, ) vminterface.full_clean() vminterface.save() @@ -92,10 +92,10 @@ def add_addr(addr, family): a.full_clean() a.save() self.log_info(f"{result} IP address {a.address} {a.vrf or ''}") - setattr(vm, f"primary_ip{family}", a) + setattr(virtual_machine, f"primary_ip{family}", a) add_addr(data["primary_ip4"], 4) add_addr(data["primary_ip6"], 6) - vm.full_clean() - vm.save() - self.log_success(f"Created VM [{vm.name}](/virtualization/virtual-machines/{vm.id}/)") + virtual_machine.full_clean() + virtual_machine.save() + self.log_success(f"Created VM [{virtual_machine.name}](/virtualization/virtual-machines/{virtual_machine.id}/)") From d4b866b847e88cec4c92f0f378d67fd2ceeac379 Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:34:45 +0000 Subject: [PATCH 17/21] Use PEP-8 style naming for vm_interface --- scripts/create_vm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index ee4f9a9..1d8a531 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -60,13 +60,13 @@ def run(self, data, commit): # pylint: disable=unused-argument virtual_machine.save() virtual_machine.tags.set(data["vm_tags"]) - vminterface = VMInterface( + vm_interface = VMInterface( name=data["interface_name"], mac_address=data["mac_address"], virtual_machine=virtual_machine, ) - vminterface.full_clean() - vminterface.save() + vm_interface.full_clean() + vm_interface.save() def add_addr(addr, family): if not addr: @@ -87,7 +87,7 @@ def add_addr(addr, family): a.dns_name = data["dns_name"] if a.assigned_object: raise RuntimeError(f"Address {addr} is already assigned") - a.assigned_object = vminterface + a.assigned_object = vm_interface a.tenant = data.get("tenant") a.full_clean() a.save() From 99dc6b65709a984e48916e686c7caa6389d3111d Mon Sep 17 00:00:00 2001 From: James Adams Date: Thu, 19 Jan 2023 13:35:12 +0000 Subject: [PATCH 18/21] Use PEP-8 style naming for ip_address --- scripts/create_vm.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 1d8a531..188aef4 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -74,25 +74,25 @@ def add_addr(addr, family): if addr.version != family: raise RuntimeError(f"Wrong family for {addr}") try: - a = IPAddress.objects.get( + ip_address = IPAddress.objects.get( address=addr, ) result = "Assigned" except ObjectDoesNotExist: - a = IPAddress( + ip_address = IPAddress( address=addr, ) result = "Created" - a.status = IPAddressStatusChoices.STATUS_ACTIVE - a.dns_name = data["dns_name"] - if a.assigned_object: + ip_address.status = IPAddressStatusChoices.STATUS_ACTIVE + ip_address.dns_name = data["dns_name"] + if ip_address.assigned_object: raise RuntimeError(f"Address {addr} is already assigned") - a.assigned_object = vm_interface - a.tenant = data.get("tenant") - a.full_clean() - a.save() - self.log_info(f"{result} IP address {a.address} {a.vrf or ''}") - setattr(virtual_machine, f"primary_ip{family}", a) + ip_address.assigned_object = vm_interface + ip_address.tenant = data.get("tenant") + ip_address.full_clean() + ip_address.save() + self.log_info(f"{result} IP address {ip_address.address} {ip_address.vrf or ''}") + setattr(virtual_machine, f"primary_ip{family}", ip_address) add_addr(data["primary_ip4"], 4) add_addr(data["primary_ip6"], 6) From 2e26ff2d6f9e5e4dbf2392de2719421b6d43d04c Mon Sep 17 00:00:00 2001 From: James Adams Date: Fri, 20 Jan 2023 11:31:01 +0000 Subject: [PATCH 19/21] Allow the field configuration to be customised --- scripts/create_vm.py | 62 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 188aef4..84f6f36 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -21,27 +21,63 @@ from virtualization.models import Cluster, VirtualMachine, VMInterface +field_config = { + 'dns_name': {'required': False}, + 'vm_tags': {'required': False}, + 'primary_ip4': {'required': False}, + 'primary_ip6': {'required': False}, + 'role': {'required': False}, + 'tenant': {'required': False}, + 'platform': {'required': False}, + 'interface_name': {'default': 'eth0'}, + 'mac_address': {'required': False}, + 'vcpus': {'required': False}, + 'memory': {'required': False}, + 'disk': {'required': False}, + 'comments': {'required': False}, +} + +# Allow the field configuration to be customised by a site specific YAML file +# For example: +# --- +# dns_name: +# required: True +# +# interface_name: +# regex: '^eth[0-9]+$' +# --- +try: + field_config_custom = Script().load_yaml('create_vm.yaml') + if isinstance(field_config_custom, dict): + # Merge field configuration, but don't allow arbitrary fields to be added to the YAML file + for field_name, field_properties in field_config.items(): + if field_name in field_config_custom: + field_properties.update(field_config_custom[field_name]) +except FileNotFoundError: + pass + + class NewVM(Script): class Meta: name = "New VM" description = "Create a new VM" vm_name = StringVar(label="VM name") - dns_name = StringVar(label="DNS name", required=False) - vm_tags = MultiObjectVar(model=Tag, label="VM tags", required=False) - primary_ip4 = IPAddressWithMaskVar(label="IPv4 address") - primary_ip6 = IPAddressWithMaskVar(label="IPv6 address", required=False) - role = ObjectVar(model=DeviceRole, query_params=dict(vm_role=True), required=False) + dns_name = StringVar(label="DNS name", **field_config['dns_name']) + vm_tags = MultiObjectVar(model=Tag, label="VM tags", **field_config['vm_tags']) + primary_ip4 = IPAddressWithMaskVar(label="IPv4 address", **field_config['primary_ip4']) + primary_ip6 = IPAddressWithMaskVar(label="IPv6 address", **field_config['primary_ip6']) + role = ObjectVar(model=DeviceRole, query_params=dict(vm_role=True), **field_config['role']) status = ChoiceVar(VirtualMachineStatusChoices, default=VirtualMachineStatusChoices.STATUS_ACTIVE) cluster = ObjectVar(model=Cluster) - tenant = ObjectVar(model=Tenant, required=False) - platform = ObjectVar(model=Platform, required=False) - interface_name = StringVar(default="eth0") - mac_address = StringVar(label="MAC address", required=False) - vcpus = IntegerVar(label="VCPUs", required=False) - memory = IntegerVar(label="Memory (MB)", required=False) - disk = IntegerVar(label="Disk (GB)", required=False) - comments = TextVar(label="Comments", required=False) + tenant = ObjectVar(model=Tenant, **field_config['tenant']) + platform = ObjectVar(model=Platform, **field_config['platform']) + interface_name = StringVar(**field_config['interface_name']) + mac_address = StringVar(label="MAC address", **field_config['mac_address']) + vcpus = IntegerVar(label="VCPUs", **field_config['vcpus']) + memory = IntegerVar(label="Memory (MB)", **field_config['memory']) + disk = IntegerVar(label="Disk (GB)", **field_config['disk']) + comments = TextVar(label="Comments", **field_config['comments']) def run(self, data, commit): # pylint: disable=unused-argument virtual_machine = VirtualMachine( From a45f8821739ef94efcd2054f7ede84b7d3e76b99 Mon Sep 17 00:00:00 2001 From: James Adams Date: Fri, 20 Jan 2023 11:31:48 +0000 Subject: [PATCH 20/21] Allow custom fields to be specified in YAML file --- scripts/create_vm.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 84f6f36..5299ed7 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -35,6 +35,7 @@ 'memory': {'required': False}, 'disk': {'required': False}, 'comments': {'required': False}, + 'custom_fields': {}, } # Allow the field configuration to be customised by a site specific YAML file @@ -46,6 +47,17 @@ # interface_name: # regex: '^eth[0-9]+$' # --- +# +# Custom fields can also be specified, type will default to String if not specified. +# Any other valid arguments to the constructor can be provided. +# For example: +# --- +# custom_fields: +# vmid: +# label: VM ID +# required: True +# regex: ^vm-[0-9]+$ +# --- try: field_config_custom = Script().load_yaml('create_vm.yaml') if isinstance(field_config_custom, dict): @@ -77,6 +89,17 @@ class Meta: vcpus = IntegerVar(label="VCPUs", **field_config['vcpus']) memory = IntegerVar(label="Memory (MB)", **field_config['memory']) disk = IntegerVar(label="Disk (GB)", **field_config['disk']) + + # Add custom fields + custom_fields = {} + for cf_name, cf_properties in field_config['custom_fields'].items(): + cls = 'StringVar' + if 'type' in cf_properties: + cls = cf_properties['type'].title() + 'Var' + del cf_properties['type'] + + vars()[f'cf_{cf_name}'] = globals()[cls](**cf_properties) + comments = TextVar(label="Comments", **field_config['comments']) def run(self, data, commit): # pylint: disable=unused-argument @@ -96,6 +119,9 @@ def run(self, data, commit): # pylint: disable=unused-argument virtual_machine.save() virtual_machine.tags.set(data["vm_tags"]) + for cf_name in field_config['custom_fields']: + virtual_machine.custom_field_data[cf_name] = data[f'cf_{cf_name}'] + vm_interface = VMInterface( name=data["interface_name"], mac_address=data["mac_address"], From 99cc1235409c4582f92491d4fc58348fabcdf69c Mon Sep 17 00:00:00 2001 From: James Adams Date: Fri, 20 Jan 2023 12:35:43 +0000 Subject: [PATCH 21/21] Import any types used by custom fields not already imported --- scripts/create_vm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/create_vm.py b/scripts/create_vm.py index 5299ed7..80d47dc 100644 --- a/scripts/create_vm.py +++ b/scripts/create_vm.py @@ -98,6 +98,10 @@ class Meta: cls = cf_properties['type'].title() + 'Var' del cf_properties['type'] + # Import any missing types + if cls not in globals(): + globals()[cls] = getattr(__import__('extras.scripts', fromlist=['extras']), cls) + vars()[f'cf_{cf_name}'] = globals()[cls](**cf_properties) comments = TextVar(label="Comments", **field_config['comments'])