コード例 #1
0
ファイル: myscripts.py プロジェクト: yangchunjian/netbox
class DemoScript(Script):
    name = "Script Demo"
    description = "A quick demonstration of the available field types"

    my_string1 = StringVar(
        description="Input a string between 3 and 10 characters",
        min_length=3,
        max_length=10)
    my_string2 = StringVar(
        description=
        "This field enforces a regex: three letters followed by three numbers",
        regex=r'[a-z]{3}\d{3}')
    my_number = IntegerVar(
        description="Pick a number between 1 and 255 (inclusive)",
        min_value=1,
        max_value=255)
    my_boolean = BooleanVar(
        description="Use the checkbox to toggle true/false")
    my_object = ObjectVar(description="Select a NetBox site",
                          queryset=Site.objects.all())

    def run(self, data):

        self.log_info("Your string was {}".format(data['my_string1']))
        self.log_info("Your second string was {}".format(data['my_string2']))
        self.log_info("Your number was {}".format(data['my_number']))
        if data['my_boolean']:
            self.log_info("You ticked the checkbox")
        else:
            self.log_info("You did not tick the checkbox")
        self.log_info("You chose the sites {}".format(data['my_object']))

        return "Here's some output"
コード例 #2
0
class SiteGeoAll(Script):
    class Meta:
        name = 'All sites for a region'
        description = 'Retrieve list of all sites and populate the latitude/longitude fields based on their physical address.'
        commit_default = True

    region = ObjectVar(model=Region, display_field=name)
    overwrite = BooleanVar(
        default=False,
        label='Override existing value',
        description='If location already exists, update the value.')

    def run(self, data, commit):
        for site in get_sites_for_region(data['region']):
            update_site(self, site)
コード例 #3
0
class SiteGeoOne(Script):
    class Meta:
        name = 'Specific site'
        description = 'Populate the latitude/longitude fields for a specific site based on its physical address.'
        commit_default = True

    location = ObjectVar(model=Site, display_field=name)
    overwrite = BooleanVar(
        default=False,
        label='Override existing value',
        description='If location already exists, update the value.')

    def run(self, data, commit):
        site = data['location']
        update_site(self, site, data['overwrite'])
コード例 #4
0
ファイル: create_vm.py プロジェクト: ryanmerolle/reports
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 = 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)
    status = ChoiceVar(VirtualMachineStatusChoices,
                       default=VirtualMachineStatusChoices.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=InterfaceTypeChoices.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,
                    vrf=data.get("vrf"),
                )
                result = "Assigned"
            except ObjectDoesNotExist:
                a = IPAddress(
                    address=addr,
                    vrf=data.get("vrf"),
                )
                result = "Created"
            a.status = IPAddressStatusChoices.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)
コード例 #5
0
class New(Script):
    class Meta:
        name = "Create FTTB CPE w/ automatic uplink selection and monitoring"
        description = "Generate standard CPE router configuration \
                       and configure uplink switch and linkage"

        field_order = [
            'business_name', 'asset_tag', 'uplink_site', 'skip_zabbix',
            'skip_uplink_port', 'confirmation_email', 'comments', 'rack_mount'
        ]

    # ##################Check to make sure you have these defined##############
    RACK_MOUNT_ID = DeviceType.objects.get(model="RB2011-RM").id
    WALL_MOUNT_ID = DeviceType.objects.get(model="RB2011").id
    SITE_ID = Site.objects.get(name="Customer Premise").id
    DEVICE_ROLE_ID = DeviceRole.objects.get(name="CPE").id
    AGG_ROLE_ID = DeviceRole.objects.get(name="Aggregation").id
    PLATFORM_ID = Platform.objects.get(name="RouterOS").id
    CHOICES = ((RACK_MOUNT_ID, "Rack Mount"), (WALL_MOUNT_ID, "Wall Mount"))
    INET_VLAN = VLAN.objects.get(vid=900)
    MGMT_VLAN = VLAN.objects.get(vid=107)
    #SITES = Site.objects.filter(region_id=1)
    REGION = 1
    ZAPI = ZabbixAPI(
        url='http://zabbix-web-apache-mysql:8080/api_jsonrpc.php/',
        user='******',
        password='******')
    SNMP_COMMUNITY = "got"
    # ##################END###################

    rack_mount = ChoiceVar(choices=((True, "Rack Mount"), (False,
                                                           "Wall Mount")))
    business_name = StringVar(label="Business Name")
    asset_tag = StringVar(label="Asset Tag", required=False)
    hardware_choice = ChoiceVar(choices=CHOICES)
    comments = TextVar(label="Comments", required=False)
    uplink_site = ObjectVar(model=Site, query_params={'region_id': REGION})
    skip_zabbix = BooleanVar(label="Disable Zabbix configuration")
    skip_uplink_port = BooleanVar(label="Disable upstream port selection")
    confirmation_email = BooleanVar(label="Send Confirmation Email")

    def run(self, data, commit):
        # Create the device
        device = Device(name=data['business_name'],
                        device_role_id=self.DEVICE_ROLE_ID,
                        device_type_id=data["hardware_choice"],
                        platform_id=self.PLATFORM_ID,
                        site_id=self.SITE_ID)
        device.save()

        interfaces = Interface.objects.filter(device_id=device.id)
        enabled_interfaces = []
        mgmt_intf = interfaces.get(name="b107")
        enabled_interfaces.append(mgmt_intf)
        uplk_intf = interfaces.get(name="ether10")
        enabled_interfaces.append(uplk_intf)
        uplk_intf.mode = "tagged"
        uplk_intf.tagged_vlans.set([self.INET_VLAN, self.MGMT_VLAN])
        uplk_intf.description = "Uplink"
        uplk_intf.save()
        inet_intf = interfaces.get(name="ether1")
        enabled_interfaces.append(inet_intf)
        inet_intf.description = "Internet"
        inet_intf.mode = "access"
        inet_intf.untagged_vlan = self.INET_VLAN
        inet_intf.save()
        mgmt_intf.save()
        for intf in interfaces:
            intf.enabled = False
            intf.save()
        for intf in enabled_interfaces:
            intf.enabled = True
            intf.mtu = 1500
            intf.save()
        available_ip = Prefix.objects.get(
            vlan=self.MGMT_VLAN).get_first_available_ip()
        ip = IPAddress(
            address=available_ip,
            assigned_object_type=ContentType.objects.get_for_model(Interface),
            assigned_object_id=mgmt_intf.id)
        ip.save()
        device.primary_ip4_id = ip.id
        device.primary_ip_id = ip.id
        device.comments = data['comments']
        device.save()

        # ############
        if (not data["skip_zabbix"] and commit):

            # Post to Zabbix API to create host in mikrotik group and ICMP
            # template
            try:
                hostid = self.ZAPI.host.create(
                    host=data["business_name"],
                    interfaces=dict(type=2,
                                    main=1,
                                    useip=1,
                                    ip=available_ip.replace("/24", ""),
                                    port=161,
                                    dns="",
                                    details=dict(
                                        version="1",
                                        bulk="0",
                                        community=self.SNMP_COMMUNITY)),
                    groups=dict(groupid=15),
                    templates=dict(templateid=10186))
                self.log_info("zabbix configured successfully")
            except Exception as e:
                self.log_info("failed to configure zabbix {0}".format(e))

        if (not data["skip_uplink_port"] and commit):
            try:
                agg_switches = Device.objects.filter(
                    site=data["uplink_site"],
                    device_role_id=self.AGG_ROLE_ID,
                    status="active")
                selected_interface = ""
                for agg_switch in agg_switches:
                    interfaces = Interface.objects.filter(
                        device_id=agg_switch.id)
                    for interface in interfaces:
                        if (interface.connection_status is not True
                                and interface.enabled is True
                                and interface.description == ''
                                and interface.type == '1000base-x-sfp'):
                            selected_interface = interface
                            break
                    if selected_interface != "":
                        selected_interface.enabled = True
                        selected_interface.description = device.name
                        selected_interface.mode = "tagged"
                        selected_interface.tagged_vlans.set(
                            [self.INET_VLAN, self.MGMT_VLAN])
                        selected_interface.save()
                        cable = Cable(
                            termination_a=uplk_intf,
                            termination_b=selected_interface,
                        )
                        cable.save()
                        self.log_info(
                            "uplink switch chosen. Port {0} on {1}".format(
                                selected_interface.name, agg_switch.name))
                        break
                if selected_interface == "":
                    self.log_failure("No available aggregate port found. \
                        No aggregate port assigned.")

            except BaseException:
                self.log("failed to document uplink switch")

        self.log_success("Created {0}".format(device.name))
コード例 #6
0
class MultiConnect(Script):
    class Meta:
        name = "Multi Connect"
        description = "Add multiple connections from one device to another"

    device_a = ObjectVar(model=Device, label="Device A")
    termination_type_a = ChoiceVar(choices=TERM_CHOICES,
                                   label="Device A port type")
    termination_name_a = StringVar(label="Device A port name pattern",
                                   description="Example: ge-0/0/[5,7,12-23]")

    device_b = ObjectVar(model=Device, label="Device B")
    termination_type_b = ChoiceVar(choices=TERM_CHOICES,
                                   label="Device B port type")
    termination_name_b = StringVar(label="Device B port name pattern",
                                   description="Example: ge-0/0/[5,7,12-23]")

    cable_status = ChoiceVar(choices=LinkStatusChoices.CHOICES,
                             default=LinkStatusChoices.STATUS_CONNECTED,
                             label="Cable Status")
    cable_type = ChoiceVar(choices=NO_CHOICE + CableTypeChoices.CHOICES,
                           required=False,
                           label="Cable Type")
    cable_tenant = ObjectVar(model=Tenant,
                             required=False,
                             label="Cable Tenant")
    cable_label = StringVar(label="Cable Label pattern", required=False)
    cable_color = ChoiceVar(choices=NO_CHOICE + ColorChoices.CHOICES,
                            required=False,
                            label="Cable Color")
    cable_length = IntegerVar(
        required=False,
        label="Cable Length")  # unfortunately there is no DecimalVar
    cable_length_unit = ChoiceVar(choices=NO_CHOICE +
                                  CableLengthUnitChoices.CHOICES,
                                  required=False,
                                  label="Cable Length Unit")
    cable_tags = MultiObjectVar(model=Tag, required=False, label="Cable Tags")

    def run(self, data, commit):
        device_a = data["device_a"]
        device_b = data["device_b"]
        ports_a = getattr(device_a, data["termination_type_a"]).all()
        ports_b = getattr(device_b, data["termination_type_b"]).all()

        terms_a = expand_pattern(data["termination_name_a"])
        terms_b = expand_pattern(data["termination_name_b"])
        if len(terms_a) != len(terms_b):
            return self.log_failure(
                f'Mismatched number of ports: {len(terms_a)} (A) versus {len(terms_b)} (B)'
            )
        labels = expand_pattern(data["cable_label"])
        if len(labels) == 1:
            labels = [labels[0] for i in range(len(terms_a))]
        elif len(labels) != len(terms_a):
            return self.log_failure(
                f'Mismatched number of labels: {len(labels)} labels versus {len(terms_a)} ports'
            )

        for i in range(len(terms_a)):
            term_a = [x for x in ports_a if x.name == terms_a[i]]
            if len(term_a) != 1:
                self.log_failure(
                    f'Unable to find "{terms_a[i]}" in {data["termination_type_a"]} on device A ({device_a.name})'
                )
                continue
            term_b = [x for x in ports_b if x.name == terms_b[i]]
            if len(term_b) != 1:
                self.log_failure(
                    f'Unable to find "{terms_b[i]}" in {data["termination_type_b"]} on device B ({device_b.name})'
                )
                continue
            cable = Cable(
                termination_a=term_a[0],
                termination_b=term_b[0],
                type=data["cable_type"],
                status=data["cable_status"],
                tenant=data["cable_tenant"],
                label=labels[i],
                color=data["cable_color"],
                length=data["cable_length"],
                length_unit=data["cable_length_unit"],
            )
            try:
                with transaction.atomic():
                    cable.full_clean()
                    cable.save()
                    cable.tags.set(data["cable_tags"])
            except Exception as e:
                self.log_failure(
                    f'Unable to connect {device_a.name}:{terms_a[i]} to {device_b.name}:{terms_b[i]}: {e}'
                )
                continue
            self.log_success(
                f'Created cable from {device_a.name}:{terms_a[i]} to {device_b.name}:{terms_b[i]}'
            )
class CreateManagementInterface(Script):
    class Meta:
        name = "Create Management Interface"
        description = "Create a management interface for a specified device and assign an IP address."

    device = ObjectVar(
        description="The Device to add management interface to",
        queryset=Device.objects.filter(device_role__slug="server"),
    )
    add_ip = BooleanVar(
        description=
        "Automatically add IP address from appropriate management network at site.",
        default=True)

    def _add_ip_to_interface(self, device, interface):
        # determine prefix appropriate to site of device
        try:
            prefix = Prefix.objects.get(site=device.site,
                                        role__slug="management",
                                        tenant=device.tenant)
        except ObjectDoesNotExist:
            message = "Can't find prefix for site {} on device {}".format(
                device.site.slug, device.name)
            self.log_failure(message)
            return message
        self.log_info("Selecting address from network {}".format(
            prefix.prefix))
        available_ips = iter(prefix.get_available_ips())

        # disable 0net skipping on frack
        if device.tenant and device.tenant.slug == 'fr-tech':
            zeroth_net = None
        else:
            # skip the first /24 net as this is reserved for network devices
            zeroth_net = list(
                ipaddress.ip_network(prefix.prefix).subnets(new_prefix=24))[0]

        ip = None
        for ip in available_ips:
            address = ipaddress.ip_address(ip)
            if zeroth_net is None or address not in zeroth_net:
                break
            else:
                ip = None

        if ip:
            # create IP address as child of appropriate prefix
            newip = IPAddress(
                address="{}/{}".format(ip, prefix.prefix.prefixlen),
                status=IPADDRESS_STATUS_ACTIVE,
                family=prefix.family,
            )
            # save ASAP
            newip.save()
            newip.vrf = prefix.vrf.pk if prefix.vrf else None
            # assign ip to interface
            newip.interface = interface
            newip.tenant = device.tenant
            newip.save()

            message = "Created ip {} for mgmt on device {}".format(
                newip, device.name)
            self.log_success(message)
            return message

        # fall through to failure
        message = "Not enough IPs to allocate one on prefix {}".format(
            prefix.prefix)
        self.log_failure(message)
        return message

    def run(self, data):
        """Create a 'mgmt' interface, and, if requested, allocate an appropriate IP address."""
        device = data['device']

        try:
            mgmt = device.interfaces.get(name='mgmt')
            self.log_info("mgmt already exists for device {}".format(
                device.name))
        except ObjectDoesNotExist:
            # create interface of name mgmt, is_mgmt flag set of type 1G Ethernet
            mgmt = Interface(name="mgmt",
                             mgmt_only=True,
                             device=device,
                             type=IFACE_TYPE_1GE_FIXED)
            mgmt.save()

        if data['add_ip']:
            return self._add_ip_to_interface(device, mgmt)

        else:
            message = "Created mgmt on device {}".format(device.name)
            self.log_success(message)
            return message
コード例 #8
0
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',
            'pve_host'
        ]

    vm_name = StringVar(label="VM name")
    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,
                     default=8)
    status = ChoiceVar(VirtualMachineStatusChoices,
                       default=VirtualMachineStatusChoices.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=True)
    memory = IntegerVar(label="Memory (MB)", required=True)
    disk = IntegerVar(label="Disk (GB)", required=True)
    comments = TextVar(label="Comments", required=False)
    # pve_host = ObjectVar(Device.objects.filter(cluster__name='Newtelco Cluster'), label="Proxmox Host", required=True)
    PVE_DEVICES = (('nt-pve', 'nt-pve'), ('nt-pve2', 'nt-pve2'),
                   ('nt-pve5', 'nt-pve5'), ('nt-pve6', 'nt-pve6'))
    pve_host = ChoiceVar(choices=PVE_DEVICES)

    # pve_host_ip=Device.objects.filter(name=pve_host)

    def run(self, data, commit):
        pve_host = Device.objects.filter(name=data["pve_host"])
        vm = VirtualMachine(
            name=data["vm_name"],
            role=data["role"],
            status=data["status"],
            vcpus=data["vcpus"],
            memory=data["memory"],
            disk=data["disk"],
            comments=data["comments"],
        )
        if commit:
            vm.save()

        interface = Interface(
            name=data["interface_name"],
            type=InterfaceTypeChoices.TYPE_VIRTUAL,
            virtual_machine=vm,
        )
        if commit:
            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 = IPAddressStatusChoices.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)

        def connect_pve(addr):
            self.log_info(addr)
            proxmox = ProxmoxAPI(
                addr,
                user='******',
                token_name='nb1',
                token_value='0cf6ab07-ff7e-41a3-80e4-e09e7fea6c7d',
                verify_ssl=False)
            self.log_success(proxmox.nodes.get())

        # self.log_info(data["pve_host"])
        pve_ip = str(pve_host.get().primary_ip4)[:-3]
        self.log_info(pve_ip)
        connect_pve(pve_ip)
        if commit:
            add_addr(data["primary_ip4"], 4)
            # add_addr(data["primary_ip6"], 6)
            vm.save()
            self.log_success("Created VM %s" % vm.name)
        else:
            self.log_success("Dry-run Success - Created VM %s" % vm.name)
コード例 #9
0
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_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)
    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)

    def run(self, data, commit):
        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.full_clean()
        vm.save()
        vm.tags.set(data["vm_tags"])

        vminterface = VMInterface(
            name=data["interface_name"],
            mac_address=data["mac_address"],
            virtual_machine=vm,
        )
        vminterface.full_clean()
        vminterface.save()

        def add_addr(addr, family):
            if not addr:
                return
            if addr.version != family:
                raise RuntimeError(f"Wrong family for {a}")
            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
            a.dns_name = data["dns_name"]
            if a.assigned_object:
                raise RuntimeError(f"Address {addr} is already assigned")
            a.assigned_object = vminterface
            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)

        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}/)"
        )