mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge pull request #3621 from netbox-community/451-devicetype-import
Enable YAML/JSON-based DeviceType import
This commit is contained in:
commit
c6893731ad
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# Rack types
|
# Rack types
|
||||||
RACK_TYPE_2POST = 100
|
RACK_TYPE_2POST = 100
|
||||||
RACK_TYPE_4POST = 200
|
RACK_TYPE_4POST = 200
|
||||||
@ -58,7 +57,10 @@ SUBDEVICE_ROLE_CHOICES = (
|
|||||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Interface types
|
#
|
||||||
|
# Numeric interface types
|
||||||
|
#
|
||||||
|
|
||||||
# Virtual
|
# Virtual
|
||||||
IFACE_TYPE_VIRTUAL = 0
|
IFACE_TYPE_VIRTUAL = 0
|
||||||
IFACE_TYPE_LAG = 200
|
IFACE_TYPE_LAG = 200
|
||||||
@ -113,15 +115,15 @@ IFACE_TYPE_16GFC_SFP_PLUS = 3160
|
|||||||
IFACE_TYPE_32GFC_SFP28 = 3320
|
IFACE_TYPE_32GFC_SFP28 = 3320
|
||||||
IFACE_TYPE_128GFC_QSFP28 = 3400
|
IFACE_TYPE_128GFC_QSFP28 = 3400
|
||||||
# InfiniBand
|
# InfiniBand
|
||||||
IFACE_FF_INFINIBAND_SDR = 7010
|
IFACE_TYPE_INFINIBAND_SDR = 7010
|
||||||
IFACE_FF_INFINIBAND_DDR = 7020
|
IFACE_TYPE_INFINIBAND_DDR = 7020
|
||||||
IFACE_FF_INFINIBAND_QDR = 7030
|
IFACE_TYPE_INFINIBAND_QDR = 7030
|
||||||
IFACE_FF_INFINIBAND_FDR10 = 7040
|
IFACE_TYPE_INFINIBAND_FDR10 = 7040
|
||||||
IFACE_FF_INFINIBAND_FDR = 7050
|
IFACE_TYPE_INFINIBAND_FDR = 7050
|
||||||
IFACE_FF_INFINIBAND_EDR = 7060
|
IFACE_TYPE_INFINIBAND_EDR = 7060
|
||||||
IFACE_FF_INFINIBAND_HDR = 7070
|
IFACE_TYPE_INFINIBAND_HDR = 7070
|
||||||
IFACE_FF_INFINIBAND_NDR = 7080
|
IFACE_TYPE_INFINIBAND_NDR = 7080
|
||||||
IFACE_FF_INFINIBAND_XDR = 7090
|
IFACE_TYPE_INFINIBAND_XDR = 7090
|
||||||
# Serial
|
# Serial
|
||||||
IFACE_TYPE_T1 = 4000
|
IFACE_TYPE_T1 = 4000
|
||||||
IFACE_TYPE_E1 = 4010
|
IFACE_TYPE_E1 = 4010
|
||||||
@ -227,15 +229,15 @@ IFACE_TYPE_CHOICES = [
|
|||||||
[
|
[
|
||||||
'InfiniBand',
|
'InfiniBand',
|
||||||
[
|
[
|
||||||
[IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'],
|
||||||
[IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'],
|
[IFACE_TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -382,7 +384,8 @@ CONNECTION_STATUS_CHOICES = [
|
|||||||
|
|
||||||
# Cable endpoint types
|
# Cable endpoint types
|
||||||
CABLE_TERMINATION_TYPES = [
|
CABLE_TERMINATION_TYPES = [
|
||||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
|
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
|
||||||
|
'circuittermination',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Cable types
|
# Cable types
|
||||||
@ -510,3 +513,379 @@ POWERFEED_LEG_CHOICES = (
|
|||||||
(POWERFEED_LEG_B, 'B'),
|
(POWERFEED_LEG_B, 'B'),
|
||||||
(POWERFEED_LEG_C, 'C'),
|
(POWERFEED_LEG_C, 'C'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Interface type values
|
||||||
|
#
|
||||||
|
|
||||||
|
class InterfaceTypes:
|
||||||
|
"""
|
||||||
|
Interface.type slugs
|
||||||
|
"""
|
||||||
|
# Virtual
|
||||||
|
TYPE_VIRTUAL = 'virtual'
|
||||||
|
TYPE_LAG = 'lag'
|
||||||
|
|
||||||
|
# Ethernet
|
||||||
|
TYPE_100ME_FIXED = '100base-tx'
|
||||||
|
TYPE_1GE_FIXED = '1000base-t'
|
||||||
|
TYPE_1GE_GBIC = '1000base-x-gbic'
|
||||||
|
TYPE_1GE_SFP = '1000base-x-sfp'
|
||||||
|
TYPE_2GE_FIXED = '2.5gbase-t'
|
||||||
|
TYPE_5GE_FIXED = '5gbase-t'
|
||||||
|
TYPE_10GE_FIXED = '10gbase-t'
|
||||||
|
TYPE_10GE_CX4 = '10gbase-cx4'
|
||||||
|
TYPE_10GE_SFP_PLUS = '10gbase-x-sfpp'
|
||||||
|
TYPE_10GE_XFP = '10gbase-x-xfp'
|
||||||
|
TYPE_10GE_XENPAK = '10gbase-x-xenpak'
|
||||||
|
TYPE_10GE_X2 = '10gbase-x-x2'
|
||||||
|
TYPE_25GE_SFP28 = '25gbase-x-sfp28'
|
||||||
|
TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp'
|
||||||
|
TYPE_50GE_QSFP28 = '50gbase-x-sfp28'
|
||||||
|
TYPE_100GE_CFP = '100gbase-x-cfp'
|
||||||
|
TYPE_100GE_CFP2 = '100gbase-x-cfp2'
|
||||||
|
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
|
||||||
|
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
||||||
|
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
||||||
|
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||||
|
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||||
|
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||||
|
|
||||||
|
# Wireless
|
||||||
|
TYPE_80211A = 'ieee802.11a'
|
||||||
|
TYPE_80211G = 'ieee802.11g'
|
||||||
|
TYPE_80211N = 'ieee802.11n'
|
||||||
|
TYPE_80211AC = 'ieee802.11ac'
|
||||||
|
TYPE_80211AD = 'ieee802.11ad'
|
||||||
|
|
||||||
|
# Cellular
|
||||||
|
TYPE_GSM = 'gsm'
|
||||||
|
TYPE_CDMA = 'cdma'
|
||||||
|
TYPE_LTE = 'lte'
|
||||||
|
|
||||||
|
# SONET
|
||||||
|
TYPE_SONET_OC3 = 'sonet-oc3'
|
||||||
|
TYPE_SONET_OC12 = 'sonet-oc12'
|
||||||
|
TYPE_SONET_OC48 = 'sonet-oc48'
|
||||||
|
TYPE_SONET_OC192 = 'sonet-oc192'
|
||||||
|
TYPE_SONET_OC768 = 'sonet-oc768'
|
||||||
|
TYPE_SONET_OC1920 = 'sonet-oc1920'
|
||||||
|
TYPE_SONET_OC3840 = 'sonet-oc3840'
|
||||||
|
|
||||||
|
# Fibrechannel
|
||||||
|
TYPE_1GFC_SFP = '1gfc-sfp'
|
||||||
|
TYPE_2GFC_SFP = '2gfc-sfp'
|
||||||
|
TYPE_4GFC_SFP = '4gfc-sfp'
|
||||||
|
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
||||||
|
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||||
|
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||||
|
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||||
|
|
||||||
|
# InfiniBand
|
||||||
|
TYPE_INFINIBAND_SDR = 'inifiband-sdr'
|
||||||
|
TYPE_INFINIBAND_DDR = 'inifiband-ddr'
|
||||||
|
TYPE_INFINIBAND_QDR = 'inifiband-qdr'
|
||||||
|
TYPE_INFINIBAND_FDR10 = 'inifiband-fdr10'
|
||||||
|
TYPE_INFINIBAND_FDR = 'inifiband-fdr'
|
||||||
|
TYPE_INFINIBAND_EDR = 'inifiband-edr'
|
||||||
|
TYPE_INFINIBAND_HDR = 'inifiband-hdr'
|
||||||
|
TYPE_INFINIBAND_NDR = 'inifiband-ndr'
|
||||||
|
TYPE_INFINIBAND_XDR = 'inifiband-xdr'
|
||||||
|
|
||||||
|
# Serial
|
||||||
|
TYPE_T1 = 't1'
|
||||||
|
TYPE_E1 = 'e1'
|
||||||
|
TYPE_T3 = 't3'
|
||||||
|
TYPE_E3 = 'e3'
|
||||||
|
|
||||||
|
# Stacking
|
||||||
|
TYPE_STACKWISE = 'cisco-stackwise'
|
||||||
|
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
||||||
|
TYPE_FLEXSTACK = 'cisco-flexstack'
|
||||||
|
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
|
||||||
|
TYPE_JUNIPER_VCP = 'juniper-vcp'
|
||||||
|
TYPE_SUMMITSTACK = 'extreme-summitstack'
|
||||||
|
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
|
||||||
|
TYPE_SUMMITSTACK256 = 'extreme-summitstack-256'
|
||||||
|
TYPE_SUMMITSTACK512 = 'extreme-summitstack-512'
|
||||||
|
|
||||||
|
# Other
|
||||||
|
TYPE_OTHER = 'other'
|
||||||
|
|
||||||
|
TYPE_CHOICES = (
|
||||||
|
(
|
||||||
|
'Virtual interfaces',
|
||||||
|
(
|
||||||
|
(TYPE_VIRTUAL, 'Virtual'),
|
||||||
|
(TYPE_LAG, 'Link Aggregation Group (LAG)'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Ethernet (fixed)',
|
||||||
|
(
|
||||||
|
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
|
||||||
|
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
|
||||||
|
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
|
||||||
|
(TYPE_5GE_FIXED, '5GBASE-T (5GE)'),
|
||||||
|
(TYPE_10GE_FIXED, '10GBASE-T (10GE)'),
|
||||||
|
(TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Ethernet (modular)',
|
||||||
|
(
|
||||||
|
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
|
||||||
|
(TYPE_1GE_SFP, 'SFP (1GE)'),
|
||||||
|
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
|
||||||
|
(TYPE_10GE_XFP, 'XFP (10GE)'),
|
||||||
|
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
|
||||||
|
(TYPE_10GE_X2, 'X2 (10GE)'),
|
||||||
|
(TYPE_25GE_SFP28, 'SFP28 (25GE)'),
|
||||||
|
(TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'),
|
||||||
|
(TYPE_50GE_QSFP28, 'QSFP28 (50GE)'),
|
||||||
|
(TYPE_100GE_CFP, 'CFP (100GE)'),
|
||||||
|
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
||||||
|
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||||
|
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
||||||
|
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||||
|
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||||
|
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||||
|
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Wireless',
|
||||||
|
(
|
||||||
|
(TYPE_80211A, 'IEEE 802.11a'),
|
||||||
|
(TYPE_80211G, 'IEEE 802.11b/g'),
|
||||||
|
(TYPE_80211N, 'IEEE 802.11n'),
|
||||||
|
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||||
|
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Cellular',
|
||||||
|
(
|
||||||
|
(TYPE_GSM, 'GSM'),
|
||||||
|
(TYPE_CDMA, 'CDMA'),
|
||||||
|
(TYPE_LTE, 'LTE'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'SONET',
|
||||||
|
(
|
||||||
|
(TYPE_SONET_OC3, 'OC-3/STM-1'),
|
||||||
|
(TYPE_SONET_OC12, 'OC-12/STM-4'),
|
||||||
|
(TYPE_SONET_OC48, 'OC-48/STM-16'),
|
||||||
|
(TYPE_SONET_OC192, 'OC-192/STM-64'),
|
||||||
|
(TYPE_SONET_OC768, 'OC-768/STM-256'),
|
||||||
|
(TYPE_SONET_OC1920, 'OC-1920/STM-640'),
|
||||||
|
(TYPE_SONET_OC3840, 'OC-3840/STM-1234'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'FibreChannel',
|
||||||
|
(
|
||||||
|
(TYPE_1GFC_SFP, 'SFP (1GFC)'),
|
||||||
|
(TYPE_2GFC_SFP, 'SFP (2GFC)'),
|
||||||
|
(TYPE_4GFC_SFP, 'SFP (4GFC)'),
|
||||||
|
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
||||||
|
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
||||||
|
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
||||||
|
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'InfiniBand',
|
||||||
|
(
|
||||||
|
(TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'),
|
||||||
|
(TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Serial',
|
||||||
|
(
|
||||||
|
(TYPE_T1, 'T1 (1.544 Mbps)'),
|
||||||
|
(TYPE_E1, 'E1 (2.048 Mbps)'),
|
||||||
|
(TYPE_T3, 'T3 (45 Mbps)'),
|
||||||
|
(TYPE_E3, 'E3 (34 Mbps)'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Stacking',
|
||||||
|
(
|
||||||
|
(TYPE_STACKWISE, 'Cisco StackWise'),
|
||||||
|
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
|
||||||
|
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
|
||||||
|
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
|
||||||
|
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
|
||||||
|
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
|
||||||
|
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
|
||||||
|
(TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'),
|
||||||
|
(TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Other',
|
||||||
|
(
|
||||||
|
(TYPE_OTHER, 'Other'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def slug_to_integer(cls, slug):
|
||||||
|
"""
|
||||||
|
Provide backward-compatible mapping of the type slug to integer.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
# Slug: integer
|
||||||
|
cls.TYPE_VIRTUAL: IFACE_TYPE_VIRTUAL,
|
||||||
|
cls.TYPE_LAG: IFACE_TYPE_LAG,
|
||||||
|
cls.TYPE_100ME_FIXED: IFACE_TYPE_100ME_FIXED,
|
||||||
|
cls.TYPE_1GE_FIXED: IFACE_TYPE_1GE_FIXED,
|
||||||
|
cls.TYPE_1GE_GBIC: IFACE_TYPE_1GE_GBIC,
|
||||||
|
cls.TYPE_1GE_SFP: IFACE_TYPE_1GE_SFP,
|
||||||
|
cls.TYPE_2GE_FIXED: IFACE_TYPE_2GE_FIXED,
|
||||||
|
cls.TYPE_5GE_FIXED: IFACE_TYPE_5GE_FIXED,
|
||||||
|
cls.TYPE_10GE_FIXED: IFACE_TYPE_10GE_FIXED,
|
||||||
|
cls.TYPE_10GE_CX4: IFACE_TYPE_10GE_CX4,
|
||||||
|
cls.TYPE_10GE_SFP_PLUS: IFACE_TYPE_10GE_SFP_PLUS,
|
||||||
|
cls.TYPE_10GE_XFP: IFACE_TYPE_10GE_XFP,
|
||||||
|
cls.TYPE_10GE_XENPAK: IFACE_TYPE_10GE_XENPAK,
|
||||||
|
cls.TYPE_10GE_X2: IFACE_TYPE_10GE_X2,
|
||||||
|
cls.TYPE_25GE_SFP28: IFACE_TYPE_25GE_SFP28,
|
||||||
|
cls.TYPE_40GE_QSFP_PLUS: IFACE_TYPE_40GE_QSFP_PLUS,
|
||||||
|
cls.TYPE_50GE_QSFP28: IFACE_TYPE_50GE_QSFP28,
|
||||||
|
cls.TYPE_100GE_CFP: IFACE_TYPE_100GE_CFP,
|
||||||
|
cls.TYPE_100GE_CFP2: IFACE_TYPE_100GE_CFP2,
|
||||||
|
cls.TYPE_100GE_CFP4: IFACE_TYPE_100GE_CFP4,
|
||||||
|
cls.TYPE_100GE_CPAK: IFACE_TYPE_100GE_CPAK,
|
||||||
|
cls.TYPE_100GE_QSFP28: IFACE_TYPE_100GE_QSFP28,
|
||||||
|
cls.TYPE_200GE_CFP2: IFACE_TYPE_200GE_CFP2,
|
||||||
|
cls.TYPE_200GE_QSFP56: IFACE_TYPE_200GE_QSFP56,
|
||||||
|
cls.TYPE_400GE_QSFP_DD: IFACE_TYPE_400GE_QSFP_DD,
|
||||||
|
cls.TYPE_80211A: IFACE_TYPE_80211A,
|
||||||
|
cls.TYPE_80211G: IFACE_TYPE_80211G,
|
||||||
|
cls.TYPE_80211N: IFACE_TYPE_80211N,
|
||||||
|
cls.TYPE_80211AC: IFACE_TYPE_80211AC,
|
||||||
|
cls.TYPE_80211AD: IFACE_TYPE_80211AD,
|
||||||
|
cls.TYPE_GSM: IFACE_TYPE_GSM,
|
||||||
|
cls.TYPE_CDMA: IFACE_TYPE_CDMA,
|
||||||
|
cls.TYPE_LTE: IFACE_TYPE_LTE,
|
||||||
|
cls.TYPE_SONET_OC3: IFACE_TYPE_SONET_OC3,
|
||||||
|
cls.TYPE_SONET_OC12: IFACE_TYPE_SONET_OC12,
|
||||||
|
cls.TYPE_SONET_OC48: IFACE_TYPE_SONET_OC48,
|
||||||
|
cls.TYPE_SONET_OC192: IFACE_TYPE_SONET_OC192,
|
||||||
|
cls.TYPE_SONET_OC768: IFACE_TYPE_SONET_OC768,
|
||||||
|
cls.TYPE_SONET_OC1920: IFACE_TYPE_SONET_OC1920,
|
||||||
|
cls.TYPE_SONET_OC3840: IFACE_TYPE_SONET_OC3840,
|
||||||
|
cls.TYPE_1GFC_SFP: IFACE_TYPE_1GFC_SFP,
|
||||||
|
cls.TYPE_2GFC_SFP: IFACE_TYPE_2GFC_SFP,
|
||||||
|
cls.TYPE_4GFC_SFP: IFACE_TYPE_4GFC_SFP,
|
||||||
|
cls.TYPE_8GFC_SFP_PLUS: IFACE_TYPE_8GFC_SFP_PLUS,
|
||||||
|
cls.TYPE_16GFC_SFP_PLUS: IFACE_TYPE_16GFC_SFP_PLUS,
|
||||||
|
cls.TYPE_32GFC_SFP28: IFACE_TYPE_32GFC_SFP28,
|
||||||
|
cls.TYPE_128GFC_QSFP28: IFACE_TYPE_128GFC_QSFP28,
|
||||||
|
cls.TYPE_INFINIBAND_SDR: IFACE_TYPE_INFINIBAND_SDR,
|
||||||
|
cls.TYPE_INFINIBAND_DDR: IFACE_TYPE_INFINIBAND_DDR,
|
||||||
|
cls.TYPE_INFINIBAND_QDR: IFACE_TYPE_INFINIBAND_QDR,
|
||||||
|
cls.TYPE_INFINIBAND_FDR10: IFACE_TYPE_INFINIBAND_FDR10,
|
||||||
|
cls.TYPE_INFINIBAND_FDR: IFACE_TYPE_INFINIBAND_FDR,
|
||||||
|
cls.TYPE_INFINIBAND_EDR: IFACE_TYPE_INFINIBAND_EDR,
|
||||||
|
cls.TYPE_INFINIBAND_HDR: IFACE_TYPE_INFINIBAND_HDR,
|
||||||
|
cls.TYPE_INFINIBAND_NDR: IFACE_TYPE_INFINIBAND_NDR,
|
||||||
|
cls.TYPE_INFINIBAND_XDR: IFACE_TYPE_INFINIBAND_XDR,
|
||||||
|
cls.TYPE_T1: IFACE_TYPE_T1,
|
||||||
|
cls.TYPE_E1: IFACE_TYPE_E1,
|
||||||
|
cls.TYPE_T3: IFACE_TYPE_T3,
|
||||||
|
cls.TYPE_E3: IFACE_TYPE_E3,
|
||||||
|
cls.TYPE_STACKWISE: IFACE_TYPE_STACKWISE,
|
||||||
|
cls.TYPE_STACKWISE_PLUS: IFACE_TYPE_STACKWISE_PLUS,
|
||||||
|
cls.TYPE_FLEXSTACK: IFACE_TYPE_FLEXSTACK,
|
||||||
|
cls.TYPE_FLEXSTACK_PLUS: IFACE_TYPE_FLEXSTACK_PLUS,
|
||||||
|
cls.TYPE_JUNIPER_VCP: IFACE_TYPE_JUNIPER_VCP,
|
||||||
|
cls.TYPE_SUMMITSTACK: IFACE_TYPE_SUMMITSTACK,
|
||||||
|
cls.TYPE_SUMMITSTACK128: IFACE_TYPE_SUMMITSTACK128,
|
||||||
|
cls.TYPE_SUMMITSTACK256: IFACE_TYPE_SUMMITSTACK256,
|
||||||
|
cls.TYPE_SUMMITSTACK512: IFACE_TYPE_SUMMITSTACK512,
|
||||||
|
}.get(slug)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Port type values
|
||||||
|
#
|
||||||
|
|
||||||
|
class PortTypes:
|
||||||
|
"""
|
||||||
|
FrontPort/RearPort.type slugs
|
||||||
|
"""
|
||||||
|
TYPE_8P8C = '8p8c'
|
||||||
|
TYPE_110_PUNCH = '110-punch'
|
||||||
|
TYPE_BNC = 'bnc'
|
||||||
|
TYPE_ST = 'st'
|
||||||
|
TYPE_SC = 'sc'
|
||||||
|
TYPE_SC_APC = 'sc-apc'
|
||||||
|
TYPE_FC = 'fc'
|
||||||
|
TYPE_LC = 'lc'
|
||||||
|
TYPE_LC_APC = 'lc-apc'
|
||||||
|
TYPE_MTRJ = 'mtrj'
|
||||||
|
TYPE_MPO = 'mpo'
|
||||||
|
TYPE_LSH = 'lsh'
|
||||||
|
TYPE_LSH_APC = 'lsh-apc'
|
||||||
|
|
||||||
|
TYPE_CHOICES = (
|
||||||
|
(
|
||||||
|
'Copper',
|
||||||
|
(
|
||||||
|
(TYPE_8P8C, '8P8C'),
|
||||||
|
(TYPE_110_PUNCH, '110 Punch'),
|
||||||
|
(TYPE_BNC, 'BNC'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Fiber Optic',
|
||||||
|
(
|
||||||
|
(TYPE_FC, 'FC'),
|
||||||
|
(TYPE_LC, 'LC'),
|
||||||
|
(TYPE_LC_APC, 'LC/APC'),
|
||||||
|
(TYPE_LSH, 'LSH'),
|
||||||
|
(TYPE_LSH_APC, 'LSH/APC'),
|
||||||
|
(TYPE_MPO, 'MPO'),
|
||||||
|
(TYPE_MTRJ, 'MTRJ'),
|
||||||
|
(TYPE_SC, 'SC'),
|
||||||
|
(TYPE_SC_APC, 'SC/APC'),
|
||||||
|
(TYPE_ST, 'ST'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def slug_to_integer(cls, slug):
|
||||||
|
"""
|
||||||
|
Provide backward-compatible mapping of the type slug to integer.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
# Slug: integer
|
||||||
|
cls.TYPE_8P8C: PORT_TYPE_8P8C,
|
||||||
|
cls.TYPE_110_PUNCH: PORT_TYPE_8P8C,
|
||||||
|
cls.TYPE_BNC: PORT_TYPE_BNC,
|
||||||
|
cls.TYPE_ST: PORT_TYPE_ST,
|
||||||
|
cls.TYPE_SC: PORT_TYPE_SC,
|
||||||
|
cls.TYPE_SC_APC: PORT_TYPE_SC_APC,
|
||||||
|
cls.TYPE_FC: PORT_TYPE_FC,
|
||||||
|
cls.TYPE_LC: PORT_TYPE_LC,
|
||||||
|
cls.TYPE_LC_APC: PORT_TYPE_LC_APC,
|
||||||
|
cls.TYPE_MTRJ: PORT_TYPE_MTRJ,
|
||||||
|
cls.TYPE_MPO: PORT_TYPE_MPO,
|
||||||
|
cls.TYPE_LSH: PORT_TYPE_LSH,
|
||||||
|
cls.TYPE_LSH_APC: PORT_TYPE_LSH_APC,
|
||||||
|
}.get(slug)
|
||||||
|
@ -23,7 +23,7 @@ from utilities.forms import (
|
|||||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
||||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
|
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
|
||||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .constants import *
|
from .constants import *
|
||||||
@ -828,29 +828,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeCSVForm(forms.ModelForm):
|
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||||
manufacturer = forms.ModelChoiceField(
|
manufacturer = forms.ModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=True,
|
to_field_name='name'
|
||||||
to_field_name='name',
|
|
||||||
help_text='Manufacturer name',
|
|
||||||
error_messages={
|
|
||||||
'invalid_choice': 'Manufacturer not found.',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
subdevice_role = CSVChoiceField(
|
|
||||||
choices=SUBDEVICE_ROLE_CHOICES,
|
|
||||||
required=False,
|
|
||||||
help_text='Parent/child status'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = DeviceType.csv_headers
|
fields = [
|
||||||
help_texts = {
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||||
'model': 'Model name',
|
]
|
||||||
'slug': 'URL-friendly slug',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||||
@ -1232,6 +1220,139 @@ class DeviceBayTemplateCreateForm(ComponentForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Component template import forms
|
||||||
|
#
|
||||||
|
|
||||||
|
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, device_type, data=None, *args, **kwargs):
|
||||||
|
|
||||||
|
# Must pass the parent DeviceType on form initialization
|
||||||
|
data.update({
|
||||||
|
'device_type': device_type.pk,
|
||||||
|
})
|
||||||
|
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
|
def clean_device_type(self):
|
||||||
|
|
||||||
|
data = self.cleaned_data['device_type']
|
||||||
|
|
||||||
|
# Limit fields referencing other components to the parent DeviceType
|
||||||
|
for field_name, field in self.fields.items():
|
||||||
|
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
|
||||||
|
field.queryset = field.queryset.filter(device_type=data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConsolePortTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ConsoleServerPortTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerPortTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name', 'maximum_draw', 'allocated_draw',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
power_port = forms.ModelChoiceField(
|
||||||
|
queryset=PowerPortTemplate.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerOutletTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name', 'power_port', 'feed_leg',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=InterfaceTypes.TYPE_CHOICES
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InterfaceTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name', 'type', 'mgmt_only',
|
||||||
|
]
|
||||||
|
|
||||||
|
def clean_type(self):
|
||||||
|
# Convert slug value to field integer value
|
||||||
|
slug = self.cleaned_data['type']
|
||||||
|
return InterfaceTypes.slug_to_integer(slug)
|
||||||
|
|
||||||
|
|
||||||
|
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=PortTypes.TYPE_CHOICES
|
||||||
|
)
|
||||||
|
rear_port = forms.ModelChoiceField(
|
||||||
|
queryset=RearPortTemplate.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FrontPortTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
|
||||||
|
]
|
||||||
|
|
||||||
|
def clean_type(self):
|
||||||
|
# Convert slug value to field integer value
|
||||||
|
slug = self.cleaned_data['type']
|
||||||
|
return PortTypes.slug_to_integer(slug)
|
||||||
|
|
||||||
|
|
||||||
|
class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=PortTypes.TYPE_CHOICES
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RearPortTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name', 'type', 'positions',
|
||||||
|
]
|
||||||
|
|
||||||
|
def clean_type(self):
|
||||||
|
# Convert slug value to field integer value
|
||||||
|
slug = self.cleaned_data['type']
|
||||||
|
return PortTypes.slug_to_integer(slug)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBayTemplate
|
||||||
|
fields = [
|
||||||
|
'device_type', 'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
|
@ -3,10 +3,11 @@ import urllib.parse
|
|||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.constants import CABLE_TYPE_CAT6, IFACE_TYPE_1GE_FIXED
|
from dcim.constants import *
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
|
Cable, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||||
RackReservation, RackRole, Site, Region, VirtualChassis,
|
FrontPortTemplate, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerPortTemplate,
|
||||||
|
PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPortTemplate, Site, Region, VirtualChassis,
|
||||||
)
|
)
|
||||||
from utilities.testing import create_test_user
|
from utilities.testing import create_test_user
|
||||||
|
|
||||||
@ -221,6 +222,132 @@ class DeviceTypeTestCase(TestCase):
|
|||||||
response = self.client.get(devicetype.get_absolute_url())
|
response = self.client.get(devicetype.get_absolute_url())
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_devicetype_import(self):
|
||||||
|
|
||||||
|
IMPORT_DATA = """
|
||||||
|
manufacturer: Generic
|
||||||
|
model: TEST-1000
|
||||||
|
slug: test-1000
|
||||||
|
u_height: 2
|
||||||
|
console-ports:
|
||||||
|
- name: Console Port 1
|
||||||
|
- name: Console Port 2
|
||||||
|
- name: Console Port 3
|
||||||
|
console-server-ports:
|
||||||
|
- name: Console Server Port 1
|
||||||
|
- name: Console Server Port 2
|
||||||
|
- name: Console Server Port 3
|
||||||
|
power-ports:
|
||||||
|
- name: Power Port 1
|
||||||
|
- name: Power Port 2
|
||||||
|
- name: Power Port 3
|
||||||
|
power-outlets:
|
||||||
|
- name: Power Outlet 1
|
||||||
|
power_port: Power Port 1
|
||||||
|
feed_leg: 1
|
||||||
|
- name: Power Outlet 2
|
||||||
|
power_port: Power Port 1
|
||||||
|
feed_leg: 1
|
||||||
|
- name: Power Outlet 3
|
||||||
|
power_port: Power Port 1
|
||||||
|
feed_leg: 1
|
||||||
|
interfaces:
|
||||||
|
- name: Interface 1
|
||||||
|
type: 1000base-t
|
||||||
|
mgmt_only: true
|
||||||
|
- name: Interface 2
|
||||||
|
type: 1000base-t
|
||||||
|
- name: Interface 3
|
||||||
|
type: 1000base-t
|
||||||
|
rear-ports:
|
||||||
|
- name: Rear Port 1
|
||||||
|
type: 8p8c
|
||||||
|
- name: Rear Port 2
|
||||||
|
type: 8p8c
|
||||||
|
- name: Rear Port 3
|
||||||
|
type: 8p8c
|
||||||
|
front-ports:
|
||||||
|
- name: Front Port 1
|
||||||
|
type: 8p8c
|
||||||
|
rear_port: Rear Port 1
|
||||||
|
- name: Front Port 2
|
||||||
|
type: 8p8c
|
||||||
|
rear_port: Rear Port 2
|
||||||
|
- name: Front Port 3
|
||||||
|
type: 8p8c
|
||||||
|
rear_port: Rear Port 3
|
||||||
|
device-bays:
|
||||||
|
- name: Device Bay 1
|
||||||
|
- name: Device Bay 2
|
||||||
|
- name: Device Bay 3
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create the manufacturer
|
||||||
|
Manufacturer(name='Generic', slug='generic').save()
|
||||||
|
|
||||||
|
# Authenticate as user with necessary permissions
|
||||||
|
user = create_test_user(username='testuser2', permissions=[
|
||||||
|
'dcim.view_devicetype',
|
||||||
|
'dcim.add_devicetype',
|
||||||
|
'dcim.add_consoleporttemplate',
|
||||||
|
'dcim.add_consoleserverporttemplate',
|
||||||
|
'dcim.add_powerporttemplate',
|
||||||
|
'dcim.add_poweroutlettemplate',
|
||||||
|
'dcim.add_interfacetemplate',
|
||||||
|
'dcim.add_frontporttemplate',
|
||||||
|
'dcim.add_rearporttemplate',
|
||||||
|
'dcim.add_devicebaytemplate',
|
||||||
|
])
|
||||||
|
self.client.force_login(user)
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
'data': IMPORT_DATA,
|
||||||
|
'format': 'yaml'
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
dt = DeviceType.objects.get(model='TEST-1000')
|
||||||
|
|
||||||
|
# Verify all of the components were created
|
||||||
|
self.assertEqual(dt.consoleport_templates.count(), 3)
|
||||||
|
cp1 = ConsolePortTemplate.objects.first()
|
||||||
|
self.assertEqual(cp1.name, 'Console Port 1')
|
||||||
|
|
||||||
|
self.assertEqual(dt.consoleserverport_templates.count(), 3)
|
||||||
|
csp1 = ConsoleServerPortTemplate.objects.first()
|
||||||
|
self.assertEqual(csp1.name, 'Console Server Port 1')
|
||||||
|
|
||||||
|
self.assertEqual(dt.powerport_templates.count(), 3)
|
||||||
|
pp1 = PowerPortTemplate.objects.first()
|
||||||
|
self.assertEqual(pp1.name, 'Power Port 1')
|
||||||
|
|
||||||
|
self.assertEqual(dt.poweroutlet_templates.count(), 3)
|
||||||
|
po1 = PowerOutletTemplate.objects.first()
|
||||||
|
self.assertEqual(po1.name, 'Power Outlet 1')
|
||||||
|
self.assertEqual(po1.power_port, pp1)
|
||||||
|
self.assertEqual(po1.feed_leg, POWERFEED_LEG_A)
|
||||||
|
|
||||||
|
self.assertEqual(dt.interface_templates.count(), 3)
|
||||||
|
iface1 = InterfaceTemplate.objects.first()
|
||||||
|
self.assertEqual(iface1.name, 'Interface 1')
|
||||||
|
self.assertEqual(iface1.type, IFACE_TYPE_1GE_FIXED)
|
||||||
|
self.assertTrue(iface1.mgmt_only)
|
||||||
|
|
||||||
|
self.assertEqual(dt.rearport_templates.count(), 3)
|
||||||
|
rp1 = RearPortTemplate.objects.first()
|
||||||
|
self.assertEqual(rp1.name, 'Rear Port 1')
|
||||||
|
|
||||||
|
self.assertEqual(dt.frontport_templates.count(), 3)
|
||||||
|
fp1 = FrontPortTemplate.objects.first()
|
||||||
|
self.assertEqual(fp1.name, 'Front Port 1')
|
||||||
|
self.assertEqual(fp1.rear_port, rp1)
|
||||||
|
self.assertEqual(fp1.rear_port_position, 1)
|
||||||
|
|
||||||
|
self.assertEqual(dt.device_bay_templates.count(), 3)
|
||||||
|
db1 = DeviceBayTemplate.objects.first()
|
||||||
|
self.assertEqual(db1.name, 'Device Bay 1')
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleTestCase(TestCase):
|
class DeviceRoleTestCase(TestCase):
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ urlpatterns = [
|
|||||||
# Device types
|
# Device types
|
||||||
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||||
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||||
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
|
||||||
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||||
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||||
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -26,7 +27,7 @@ from utilities.paginator import EnhancedPaginator
|
|||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
||||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
@ -653,11 +654,31 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
default_return_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
|
||||||
permission_required = 'dcim.add_devicetype'
|
permission_required = [
|
||||||
model_form = forms.DeviceTypeCSVForm
|
'dcim.add_devicetype',
|
||||||
table = tables.DeviceTypeTable
|
'dcim.add_consoleporttemplate',
|
||||||
default_return_url = 'dcim:devicetype_list'
|
'dcim.add_consoleserverporttemplate',
|
||||||
|
'dcim.add_powerporttemplate',
|
||||||
|
'dcim.add_poweroutlettemplate',
|
||||||
|
'dcim.add_interfacetemplate',
|
||||||
|
'dcim.add_frontporttemplate',
|
||||||
|
'dcim.add_rearporttemplate',
|
||||||
|
'dcim.add_devicebaytemplate',
|
||||||
|
]
|
||||||
|
model = DeviceType
|
||||||
|
model_form = forms.DeviceTypeImportForm
|
||||||
|
related_object_forms = OrderedDict((
|
||||||
|
('console-ports', forms.ConsolePortTemplateImportForm),
|
||||||
|
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
||||||
|
('power-ports', forms.PowerPortTemplateImportForm),
|
||||||
|
('power-outlets', forms.PowerOutletTemplateImportForm),
|
||||||
|
('interfaces', forms.InterfaceTemplateImportForm),
|
||||||
|
('rear-ports', forms.RearPortTemplateImportForm),
|
||||||
|
('front-ports', forms.FrontPortTemplateImportForm),
|
||||||
|
('device-bays', forms.DeviceBayTemplateImportForm),
|
||||||
|
))
|
||||||
|
default_return_url = 'dcim:devicetype_import'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
{% extends 'utilities/obj_bulk_import.html' %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
{% include 'dcim/inc/device_import_header.html' %}
|
{% include 'dcim/inc/device_import_header.html' %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
{% extends 'utilities/obj_bulk_import.html' %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
{% extends 'utilities/obj_bulk_import.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}</h1>
|
||||||
|
{% block tabs %}{% endblock %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-7">
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form action="" method="post" class="form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% render_form form %}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-12 text-right">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
{% if return_url %}
|
||||||
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
{% if fields %}
|
||||||
|
<h4 class="text-center">CSV Format</h4>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
{% for name, field in fields.items %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ name }}</code></td>
|
||||||
|
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
{{ field.help_text|default:field.label }}
|
||||||
|
{% if field.choices %}
|
||||||
|
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||||
|
{% elif field|widget_type == 'dateinput' %}
|
||||||
|
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||||
|
{% elif field|widget_type == 'checkboxinput' %}
|
||||||
|
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -6,7 +6,7 @@
|
|||||||
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
||||||
{% block tabs %}{% endblock %}
|
{% block tabs %}{% endblock %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-7">
|
<div class="col-md-8 col-md-offset-2">
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading"><strong>Errors</strong></div>
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
@ -15,12 +15,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="" method="post" class="form">
|
<form action="" method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-12 text-right">
|
<div class="col-md-12 text-right">
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
<button type="submit" name="_create" class="btn btn-primary">Submit</button>
|
||||||
|
<button type="submit" name="_addanother" class="btn btn-primary">Submit and Import Another</button>
|
||||||
{% if return_url %}
|
{% if return_url %}
|
||||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -28,33 +29,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
|
||||||
{% if fields %}
|
|
||||||
<h4 class="text-center">CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Required</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
{% for name, field in fields.items %}
|
|
||||||
<tr>
|
|
||||||
<td><code>{{ name }}</code></td>
|
|
||||||
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
|
||||||
<td>
|
|
||||||
{{ field.help_text|default:field.label }}
|
|
||||||
{% if field.choices %}
|
|
||||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
|
||||||
{% elif field|widget_type == 'dateinput' %}
|
|
||||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
|
||||||
{% elif field|widget_type == 'checkboxinput' %}
|
|
||||||
<br /><small class="text-muted">Specify "true" or "false"</small>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,6 +2,7 @@ import csv
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
import yaml
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -722,3 +723,41 @@ class BulkEditForm(forms.Form):
|
|||||||
# Copy any nullable fields defined in Meta
|
# Copy any nullable fields defined in Meta
|
||||||
if hasattr(self.Meta, 'nullable_fields'):
|
if hasattr(self.Meta, 'nullable_fields'):
|
||||||
self.nullable_fields = self.Meta.nullable_fields
|
self.nullable_fields = self.Meta.nullable_fields
|
||||||
|
|
||||||
|
|
||||||
|
class ImportForm(BootstrapMixin, forms.Form):
|
||||||
|
"""
|
||||||
|
Generic form for creating an object from JSON/YAML data
|
||||||
|
"""
|
||||||
|
data = forms.CharField(
|
||||||
|
widget=forms.Textarea,
|
||||||
|
help_text="Enter object data in JSON or YAML format."
|
||||||
|
)
|
||||||
|
format = forms.ChoiceField(
|
||||||
|
choices=(
|
||||||
|
('json', 'JSON'),
|
||||||
|
('yaml', 'YAML')
|
||||||
|
),
|
||||||
|
initial='yaml'
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
data = self.cleaned_data['data']
|
||||||
|
format = self.cleaned_data['format']
|
||||||
|
|
||||||
|
# Process JSON/YAML data
|
||||||
|
if format == 'json':
|
||||||
|
try:
|
||||||
|
self.cleaned_data['data'] = json.loads(data)
|
||||||
|
except json.decoder.JSONDecodeError as err:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'data': "Invalid JSON data: {}".format(err)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.cleaned_data['data'] = yaml.load(data, Loader=yaml.SafeLoader)
|
||||||
|
except yaml.scanner.ScannerError as err:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'data': "Invalid YAML data: {}".format(err)
|
||||||
|
})
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import yaml
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -24,10 +26,11 @@ from django_tables2 import RequestConfig
|
|||||||
|
|
||||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
||||||
from extras.querysets import CustomFieldQueryset
|
from extras.querysets import CustomFieldQueryset
|
||||||
|
from utilities.exceptions import AbortTransaction
|
||||||
from utilities.forms import BootstrapMixin, CSVDataField
|
from utilities.forms import BootstrapMixin, CSVDataField
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
from .error_handlers import handle_protectederror
|
from .error_handlers import handle_protectederror
|
||||||
from .forms import ConfirmationForm
|
from .forms import ConfirmationForm, ImportForm
|
||||||
from .paginator import EnhancedPaginator
|
from .paginator import EnhancedPaginator
|
||||||
|
|
||||||
|
|
||||||
@ -394,6 +397,106 @@ class BulkCreateView(GetReturnURLMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectImportView(GetReturnURLMixin, View):
|
||||||
|
"""
|
||||||
|
Import a single object (YAML or JSON format).
|
||||||
|
"""
|
||||||
|
model = None
|
||||||
|
model_form = None
|
||||||
|
related_object_forms = dict()
|
||||||
|
template_name = 'utilities/obj_import.html'
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
|
||||||
|
form = ImportForm()
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'form': form,
|
||||||
|
'obj_type': self.model._meta.verbose_name,
|
||||||
|
'return_url': self.get_return_url(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
|
||||||
|
form = ImportForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
# Initialize model form
|
||||||
|
data = form.cleaned_data['data']
|
||||||
|
model_form = self.model_form(data)
|
||||||
|
|
||||||
|
# Assign default values for any fields which were not specified. We have to do this manually because passing
|
||||||
|
# 'initial=' to the form on initialization merely sets default values for the widgets. Since widgets are not
|
||||||
|
# used for YAML/JSON import, we first bind the imported data normally, then update the form's data with the
|
||||||
|
# applicable field defaults as needed prior to form validation.
|
||||||
|
for field_name, field in model_form.fields.items():
|
||||||
|
if field_name not in data and hasattr(field, 'initial'):
|
||||||
|
model_form.data[field_name] = field.initial
|
||||||
|
|
||||||
|
if model_form.is_valid():
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
|
# Save the primary object
|
||||||
|
obj = model_form.save()
|
||||||
|
|
||||||
|
# Iterate through the related object forms (if any), validating and saving each instance.
|
||||||
|
for field_name, related_object_form in self.related_object_forms.items():
|
||||||
|
|
||||||
|
for i, rel_obj_data in enumerate(data.get(field_name, list())):
|
||||||
|
|
||||||
|
f = related_object_form(obj, rel_obj_data)
|
||||||
|
|
||||||
|
for subfield_name, field in f.fields.items():
|
||||||
|
if subfield_name not in rel_obj_data and hasattr(field, 'initial'):
|
||||||
|
f.data[subfield_name] = field.initial
|
||||||
|
|
||||||
|
if f.is_valid():
|
||||||
|
f.save()
|
||||||
|
else:
|
||||||
|
# Replicate errors on the related object form to the primary form for display
|
||||||
|
for subfield_name, errors in f.errors.items():
|
||||||
|
for err in errors:
|
||||||
|
err_msg = "{}[{}] {}: {}".format(field_name, i, subfield_name, err)
|
||||||
|
model_form.add_error(None, err_msg)
|
||||||
|
raise AbortTransaction()
|
||||||
|
|
||||||
|
except AbortTransaction:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not model_form.errors:
|
||||||
|
|
||||||
|
messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
|
||||||
|
obj.get_absolute_url(), obj
|
||||||
|
)))
|
||||||
|
|
||||||
|
if '_addanother' in request.POST:
|
||||||
|
return redirect(request.get_full_path())
|
||||||
|
|
||||||
|
return_url = form.cleaned_data.get('return_url')
|
||||||
|
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
|
||||||
|
return redirect(return_url)
|
||||||
|
else:
|
||||||
|
return redirect(self.get_return_url(request, obj))
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# Replicate model form errors for display
|
||||||
|
for field, errors in model_form.errors.items():
|
||||||
|
for err in errors:
|
||||||
|
if field == '__all__':
|
||||||
|
form.add_error(None, err)
|
||||||
|
else:
|
||||||
|
form.add_error(None, "{}: {}".format(field, err))
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'form': form,
|
||||||
|
'obj_type': self.model._meta.verbose_name,
|
||||||
|
'return_url': self.get_return_url(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkImportView(GetReturnURLMixin, View):
|
class BulkImportView(GetReturnURLMixin, View):
|
||||||
"""
|
"""
|
||||||
Import objects in bulk (CSV format).
|
Import objects in bulk (CSV format).
|
||||||
@ -405,7 +508,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
|||||||
"""
|
"""
|
||||||
model_form = None
|
model_form = None
|
||||||
table = None
|
table = None
|
||||||
template_name = 'utilities/obj_import.html'
|
template_name = 'utilities/obj_bulk_import.html'
|
||||||
widget_attrs = {}
|
widget_attrs = {}
|
||||||
|
|
||||||
def _import_form(self, *args, **kwargs):
|
def _import_form(self, *args, **kwargs):
|
||||||
|
Loading…
Reference in New Issue
Block a user