├── .gitignore
├── .idea
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
└── netbox_vcenter.iml
├── LICENSE.txt
├── README.md
├── netbox_vcenter
├── __init__.py
├── background_tasks.py
├── forms.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── update_vcenter_cache.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20200403_0215.py
│ └── __init__.py
├── models.py
├── navigation.py
├── template_content.py
├── templates
│ └── netbox_vcenter
│ │ ├── cluster
│ │ └── vcenter_info.html
│ │ └── virtualmachine
│ │ ├── vcenter_refresh.html
│ │ └── vcenter_resources.html
├── urls.py
├── validators.py
└── views.py
├── pyproject.toml
├── setup.cfg
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
5 |
6 | # User-specific stuff
7 | .idea/**/workspace.xml
8 | .idea/**/tasks.xml
9 | .idea/**/usage.statistics.xml
10 | .idea/**/dictionaries
11 | .idea/**/shelf
12 |
13 | # Generated files
14 | .idea/**/contentModel.xml
15 |
16 | # Sensitive or high-churn files
17 | .idea/**/dataSources/
18 | .idea/**/dataSources.ids
19 | .idea/**/dataSources.local.xml
20 | .idea/**/sqlDataSources.xml
21 | .idea/**/dynamic.xml
22 | .idea/**/uiDesigner.xml
23 | .idea/**/dbnavigator.xml
24 |
25 | # Gradle
26 | .idea/**/gradle.xml
27 | .idea/**/libraries
28 |
29 | # Gradle and Maven with auto-import
30 | # When using Gradle or Maven with auto-import, you should exclude module files,
31 | # since they will be recreated, and may cause churn. Uncomment if using
32 | # auto-import.
33 | # .idea/artifacts
34 | # .idea/compiler.xml
35 | # .idea/modules.xml
36 | # .idea/*.iml
37 | # .idea/modules
38 | # *.iml
39 | # *.ipr
40 |
41 | # CMake
42 | cmake-build-*/
43 |
44 | # Mongo Explorer plugin
45 | .idea/**/mongoSettings.xml
46 |
47 | # File-based project format
48 | *.iws
49 |
50 | # IntelliJ
51 | out/
52 |
53 | # mpeltonen/sbt-idea plugin
54 | .idea_modules/
55 |
56 | # JIRA plugin
57 | atlassian-ide-plugin.xml
58 |
59 | # Cursive Clojure plugin
60 | .idea/replstate.xml
61 |
62 | # Crashlytics plugin (for Android Studio and IntelliJ)
63 | com_crashlytics_export_strings.xml
64 | crashlytics.properties
65 | crashlytics-build.properties
66 | fabric.properties
67 |
68 | # Editor-based Rest Client
69 | .idea/httpRequests
70 |
71 | # Android studio 3.1+ serialized cache file
72 | .idea/caches/build_file_checksums.ser
73 |
74 | ### Python template
75 | # Byte-compiled / optimized / DLL files
76 | __pycache__/
77 | *.py[cod]
78 | *$py.class
79 |
80 | # C extensions
81 | *.so
82 |
83 | # Distribution / packaging
84 | .Python
85 | build/
86 | develop-eggs/
87 | dist/
88 | downloads/
89 | eggs/
90 | .eggs/
91 | lib/
92 | lib64/
93 | parts/
94 | sdist/
95 | var/
96 | wheels/
97 | pip-wheel-metadata/
98 | share/python-wheels/
99 | *.egg-info/
100 | .installed.cfg
101 | *.egg
102 | MANIFEST
103 |
104 | # PyInstaller
105 | # Usually these files are written by a python script from a template
106 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
107 | *.manifest
108 | *.spec
109 |
110 | # Installer logs
111 | pip-log.txt
112 | pip-delete-this-directory.txt
113 |
114 | # Unit test / coverage reports
115 | htmlcov/
116 | .tox/
117 | .nox/
118 | .coverage
119 | .coverage.*
120 | .cache
121 | nosetests.xml
122 | coverage.xml
123 | *.cover
124 | *.py,cover
125 | .hypothesis/
126 | .pytest_cache/
127 |
128 | # Translations
129 | *.mo
130 | *.pot
131 |
132 | # Django stuff:
133 | *.log
134 | local_settings.py
135 | db.sqlite3
136 | db.sqlite3-journal
137 |
138 | # Flask stuff:
139 | instance/
140 | .webassets-cache
141 |
142 | # Scrapy stuff:
143 | .scrapy
144 |
145 | # Sphinx documentation
146 | docs/_build/
147 |
148 | # PyBuilder
149 | target/
150 |
151 | # Jupyter Notebook
152 | .ipynb_checkpoints
153 |
154 | # IPython
155 | profile_default/
156 | ipython_config.py
157 |
158 | # pyenv
159 | .python-version
160 |
161 | # pipenv
162 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
163 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
164 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
165 | # install all needed dependencies.
166 | #Pipfile.lock
167 |
168 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
169 | __pypackages__/
170 |
171 | # Celery stuff
172 | celerybeat-schedule
173 | celerybeat.pid
174 |
175 | # SageMath parsed files
176 | *.sage.py
177 |
178 | # Environments
179 | .env
180 | .venv
181 | env/
182 | venv/
183 | ENV/
184 | env.bak/
185 | venv.bak/
186 |
187 | # Spyder project settings
188 | .spyderproject
189 | .spyproject
190 |
191 | # Rope project settings
192 | .ropeproject
193 |
194 | # mkdocs documentation
195 | /site
196 |
197 | # mypy
198 | .mypy_cache/
199 | .dmypy.json
200 | dmypy.json
201 |
202 | # Pyre type checker
203 | .pyre/
204 |
205 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/netbox_vcenter.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vCenter integration plugin for NetBox
2 |
3 | This plugin shows live data from vCenter clusters in NetBox, making it easier for administrators to make sure that reality reflects what is documented in NetBox.
4 |
5 | ## Compatibility
6 |
7 | This plugin in compatible with [NetBox](https://netbox.readthedocs.org/) 2.8 and later. NetBox 2.10 introduced breaking
8 | changes that make it unusable for my own use cases, so I will not be providing support for it. There is work being done
9 | to create a fork of NetBox that is friendlier to both network operators and contributors. My future work will be in
10 | support of that.
11 |
--------------------------------------------------------------------------------
/netbox_vcenter/__init__.py:
--------------------------------------------------------------------------------
1 | VERSION = '0.1.1'
2 |
3 | try:
4 | from extras.plugins import PluginConfig
5 | except ImportError:
6 | # Dummy for when importing outside of netbox
7 | class PluginConfig:
8 | pass
9 |
10 |
11 | class NetBoxVCenterConfig(PluginConfig):
12 | name = 'netbox_vcenter'
13 | verbose_name = 'vCenter'
14 | version = VERSION
15 | author = 'Sander Steffann'
16 | author_email = 'sander@steffann.nl'
17 | description = 'vCenter integration for NetBox'
18 | base_url = 'vcenter'
19 | required_settings = []
20 | default_settings = {
21 | 'CACHE_TIMEOUT': 3600,
22 | 'CACHE_FAILURE_TIMEOUT': 600,
23 | 'REFRESH_WAIT': 15,
24 | }
25 |
26 |
27 | config = NetBoxVCenterConfig
28 |
--------------------------------------------------------------------------------
/netbox_vcenter/background_tasks.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import logging
3 | import time
4 |
5 | from cacheops import CacheMiss, cache
6 | from django.conf import settings
7 | from django_rq import job
8 | from pyVim import connect
9 | from pyVmomi import vim
10 |
11 | from netbox_vcenter.models import ClusterVCenter
12 |
13 | logger = logging.getLogger('netbox_vcenter')
14 |
15 |
16 | def get_virtual_machines(vcenter: ClusterVCenter):
17 | if not vcenter:
18 | return None
19 |
20 | logger.debug("Checking for VMs on {}".format(vcenter.server))
21 | try:
22 | cache_key = get_cache_key(vcenter)
23 | vms = cache.get(cache_key)
24 | if vms != 'FAILED':
25 | logger.debug("Found cached VMs on {}".format(vcenter.server))
26 | return vms
27 | except CacheMiss:
28 | # Get the VMs in the background worker, it will fill the cache
29 | logger.info("Initiating background task to retrieve VMs from {}".format(vcenter.server))
30 | refresh_virtual_machines.delay(vcenter=vcenter)
31 |
32 | return None
33 |
34 |
35 | def get_nic_vlan(content, dvs_cache, portgroup_cache, vm, dev):
36 | dev_backing = dev.backing
37 | vlan_id = None
38 |
39 | if hasattr(dev_backing, 'port'):
40 | port_group_key = dev.backing.port.portgroupKey
41 | dvs_uuid = dev.backing.port.switchUuid
42 | if dvs_uuid in dvs_cache:
43 | dvs = dvs_cache[dvs_uuid]
44 | else:
45 | try:
46 | dvs = content.dvSwitchManager.QueryDvsByUuid(dvs_uuid)
47 | dvs_cache[dvs_uuid] = dvs
48 | except Exception:
49 | dvs = None
50 |
51 | if dvs:
52 | pg_obj = dvs.LookupDvPortGroup(port_group_key)
53 | vlan_id = str(pg_obj.config.defaultPortConfig.vlan.vlanId)
54 | else:
55 | portgroup = dev.backing.network.name
56 | vm_host = vm.runtime.host
57 | if vm_host in portgroup_cache:
58 | pgs = portgroup_cache[vm_host]
59 | else:
60 | pgs = vm_host.config.network.portgroup
61 | portgroup_cache[vm_host] = pgs
62 |
63 | for p in pgs:
64 | if portgroup in p.key:
65 | vlan_id = str(p.spec.vlanId)
66 |
67 | return vlan_id
68 |
69 |
70 | def get_objects_of_type(content, obj_type):
71 | view_mgr = content.viewManager.CreateContainerView(content.rootFolder,
72 | [obj_type],
73 | True)
74 | try:
75 | return list(view_mgr.view)
76 | finally:
77 | view_mgr.Destroy()
78 |
79 |
80 | def get_cache_key(vcenter: ClusterVCenter):
81 | raw_key = f'{vcenter.server}\t{vcenter.username}\t{vcenter.password}'
82 | key = hashlib.sha256(raw_key.encode('utf-8')).hexdigest()[-16]
83 | return key
84 |
85 |
86 | @job
87 | def refresh_virtual_machines(vcenter: ClusterVCenter, force=False):
88 | config = settings.PLUGINS_CONFIG['netbox_vcenter']
89 | vcenter_cache_key = get_cache_key(vcenter)
90 |
91 | # Check whether this server has failed recently and shouldn't be retried yet
92 | try:
93 | cached_data = cache.get(vcenter_cache_key)
94 | if not force and cached_data == 'FAILED':
95 | logger.info("Skipping vCenter update; server {} failed recently".format(vcenter.server))
96 | return
97 |
98 | if not force:
99 | logger.info("Skipping vCenter update; server {} already in cache".format(vcenter.server))
100 | return cached_data
101 | except CacheMiss:
102 | pass
103 |
104 | service_instance = None
105 | try:
106 | logger.debug("Fetching VMs from {}".format(vcenter.server))
107 |
108 | # Connect to the vCenter server
109 | if vcenter.validate_certificate:
110 | service_instance = connect.Connect(vcenter.server, user=vcenter.username, pwd=vcenter.password)
111 | else:
112 | service_instance = connect.ConnectNoSSL(vcenter.server, user=vcenter.username, pwd=vcenter.password)
113 |
114 | content = service_instance.RetrieveContent()
115 |
116 | vms = get_objects_of_type(content, vim.VirtualMachine)
117 | all_stats = {
118 | 'timestamp': time.time(),
119 | 'vms': {}
120 | }
121 | dvs_cache = {}
122 | portgroup_cache = {}
123 | for vm in vms:
124 | vm_stats = {
125 | 'power': None,
126 | 'vcpus': None,
127 | 'memory': None,
128 | 'disk': None,
129 | 'nics': [],
130 | }
131 |
132 | try:
133 | if vm.runtime.powerState:
134 | vm_stats['powered_on'] = vm.runtime.powerState == 'poweredOn'
135 | if vm.config.hardware.numCPU:
136 | vm_stats['vcpus'] = vm.config.hardware.numCPU
137 | if vm.config.hardware.memoryMB:
138 | vm_stats['memory'] = vm.config.hardware.memoryMB
139 |
140 | disk_devices = [device for device in vm.config.hardware.device
141 | if isinstance(device, vim.vm.device.VirtualDisk)]
142 | if disk_devices:
143 | # Sum and convert from KB to GB
144 | total_capacity = 0
145 | for device in disk_devices:
146 | total_capacity += device.capacityInKB
147 | vm_stats['disk'] = round(total_capacity / 1048576)
148 |
149 | for dev in vm.config.hardware.device:
150 | if isinstance(dev, vim.vm.device.VirtualEthernetCard):
151 | vlan = get_nic_vlan(content, dvs_cache, portgroup_cache, vm, dev)
152 | vm_stats['nics'].append({
153 | 'label': dev.deviceInfo.label,
154 | 'mac_address': dev.macAddress,
155 | 'vlan': vlan,
156 | })
157 | except Exception:
158 | logger.exception("Error while fetching virtual machine {} from {}".format(vm.name, vcenter.server))
159 | continue
160 |
161 | # Collect all stats for returning
162 | all_stats['vms'][vm.name] = vm_stats
163 |
164 | # Cache a list of all VMs
165 | cache.set(vcenter_cache_key, all_stats, config['CACHE_TIMEOUT'])
166 |
167 | return all_stats
168 | except Exception:
169 | # Set a cookie in the cache so we don't keep retrying
170 | logger.exception("Error while fetching virtual machines from {}. "
171 | "Disabling checks for 5 minutes.".format(vcenter.server))
172 | cache.set(vcenter_cache_key, 'FAILED', config['CACHE_FAILURE_TIMEOUT'])
173 | finally:
174 | if service_instance:
175 | connect.Disconnect(service_instance)
176 |
--------------------------------------------------------------------------------
/netbox_vcenter/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from netbox_vcenter.models import ClusterVCenter
5 | from utilities.forms import BootstrapMixin
6 |
7 |
8 | class ClusterVCenterEditForm(BootstrapMixin, forms.ModelForm):
9 | new_password = forms.CharField(
10 | max_length=64,
11 | label='Password',
12 | widget=forms.PasswordInput(),
13 | help_text=_('This password is stored unencrypted in the database. '
14 | 'Use a read-only account with limited privileges!'),
15 | )
16 |
17 | class Meta:
18 | model = ClusterVCenter
19 | fields = ['server', 'validate_certificate', 'username']
20 |
21 | def __init__(self, *args, **kwargs):
22 | super().__init__(*args, **kwargs)
23 |
24 | if self.instance and self.instance.password:
25 | # Hack the password field and make it non-required
26 | self.fields['new_password'].required = False
27 | self.fields['new_password'].widget.is_required = False
28 | if 'required' in self.fields['new_password'].widget.attrs:
29 | del self.fields['new_password'].widget.attrs['required']
30 | self.fields['new_password'].widget.attrs['placeholder'] = 'current password hidden'
31 |
32 | def _post_clean(self):
33 | # Update the password if it is provided
34 | if self.cleaned_data['new_password']:
35 | self.instance.password = self.cleaned_data['new_password']
36 |
37 | return super()._post_clean()
38 |
--------------------------------------------------------------------------------
/netbox_vcenter/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjm-steffann/netbox-vcenter/0be55315b7a6c60e2b30198ccd6cfa3d02c2cf14/netbox_vcenter/management/__init__.py
--------------------------------------------------------------------------------
/netbox_vcenter/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjm-steffann/netbox-vcenter/0be55315b7a6c60e2b30198ccd6cfa3d02c2cf14/netbox_vcenter/management/commands/__init__.py
--------------------------------------------------------------------------------
/netbox_vcenter/management/commands/update_vcenter_cache.py:
--------------------------------------------------------------------------------
1 | from django.core.management import BaseCommand
2 |
3 | from netbox_vcenter.background_tasks import refresh_virtual_machines
4 | from netbox_vcenter.models import ClusterVCenter
5 |
6 |
7 | class Command(BaseCommand):
8 | help = "Update the cache of vCenter information"
9 |
10 | def handle(self, verbosity, *args, **kwargs):
11 | for vcenter in ClusterVCenter.objects.all():
12 | if verbosity >= 1:
13 | self.stdout.write(f"Scheduling cache update for {vcenter.cluster.name}")
14 | refresh_virtual_machines.delay(vcenter=vcenter, force=True)
15 |
--------------------------------------------------------------------------------
/netbox_vcenter/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.5 on 2020-04-02 23:08
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import netbox_vcenter.validators
7 |
8 |
9 | class Migration(migrations.Migration):
10 | initial = True
11 |
12 | dependencies = [
13 | ('virtualization', '0014_standardize_description'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='ClusterVCenter',
19 | fields=[
20 | ('id', models.AutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False
24 | )),
25 | ('server', models.CharField(
26 | max_length=64,
27 | validators=[netbox_vcenter.validators.HostnameAddressValidator()]
28 | )),
29 | ('validate_certificate', models.BooleanField(
30 | default=True
31 | )),
32 | ('username', models.CharField(
33 | max_length=64
34 | )),
35 | ('password', models.CharField(
36 | max_length=64
37 | )),
38 | ('cluster', models.OneToOneField(
39 | on_delete=django.db.models.deletion.CASCADE,
40 | related_name='vcenter',
41 | to='virtualization.Cluster'
42 | )),
43 | ],
44 | ),
45 | ]
46 |
--------------------------------------------------------------------------------
/netbox_vcenter/migrations/0002_auto_20200403_0215.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.5 on 2020-04-03 00:15
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_vcenter', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='clustervcenter',
15 | options={'verbose_name': 'vCenter configuration', 'verbose_name_plural': 'vCenter configurations'},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/netbox_vcenter/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sjm-steffann/netbox-vcenter/0be55315b7a6c60e2b30198ccd6cfa3d02c2cf14/netbox_vcenter/migrations/__init__.py
--------------------------------------------------------------------------------
/netbox_vcenter/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from netbox_vcenter.validators import HostnameAddressValidator
5 | from virtualization.models import Cluster
6 |
7 |
8 | class ClusterVCenter(models.Model):
9 | cluster = models.OneToOneField(
10 | to=Cluster,
11 | on_delete=models.CASCADE,
12 | related_name='vcenter',
13 | )
14 | server = models.CharField(
15 | verbose_name=_('vCenter Server'),
16 | max_length=64,
17 | validators=[HostnameAddressValidator()],
18 | )
19 | validate_certificate = models.BooleanField(
20 | verbose_name=_('validate certificate'),
21 | default=True,
22 | )
23 | username = models.CharField(
24 | verbose_name=_('username'),
25 | max_length=64,
26 | )
27 | password = models.CharField(
28 | verbose_name=_('password'),
29 | max_length=64,
30 | )
31 |
32 | class Meta:
33 | verbose_name = _('vCenter configuration')
34 | verbose_name_plural = _('vCenter configurations')
35 |
36 | def __str__(self):
37 | return self.cluster.name
38 |
39 | def get_absolute_url(self):
40 | return self.cluster.get_absolute_url()
41 |
--------------------------------------------------------------------------------
/netbox_vcenter/navigation.py:
--------------------------------------------------------------------------------
1 | # from extras.plugins import PluginMenuItem
2 | #
3 | # menu_items = (
4 | # PluginMenuItem(
5 | # link='plugins:netbox_vcenter:compare',
6 | # link_text='Compare with vCenter',
7 | # ),
8 | # )
9 |
--------------------------------------------------------------------------------
/netbox_vcenter/template_content.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from django.contrib.auth.context_processors import PermWrapper
4 | from django.template.context_processors import csrf
5 |
6 | from extras.plugins import PluginTemplateExtension
7 | from netbox_vcenter.background_tasks import get_virtual_machines
8 | from virtualization.models import VirtualMachine
9 |
10 |
11 | # noinspection PyAbstractClass
12 | class ClusterInfo(PluginTemplateExtension):
13 | model = 'virtualization.cluster'
14 |
15 | def left_page(self):
16 | """
17 | An info-box with edit button for the vCenter settings
18 | """
19 | return self.render('netbox_vcenter/cluster/vcenter_info.html', {
20 | 'perms': PermWrapper(self.context['request'].user)
21 | })
22 |
23 |
24 | # noinspection PyAbstractClass
25 | class VirtualMachineInfo(PluginTemplateExtension):
26 | model = 'virtualization.virtualmachine'
27 |
28 | def buttons(self):
29 | context = {
30 | 'perms': PermWrapper(self.context['request'].user),
31 | }
32 | context.update(csrf(self.context['request']))
33 | return self.render('netbox_vcenter/virtualmachine/vcenter_refresh.html', context)
34 |
35 | def right_page(self):
36 | """
37 | An info-box with information from vCenter
38 | """
39 | vm = self.context['object'] # type: VirtualMachine
40 |
41 | try:
42 | all_stats = get_virtual_machines(vm.cluster.vcenter)
43 | vcenter_timestamp = datetime.fromtimestamp(all_stats['timestamp'])
44 | vcenter_resources = all_stats['vms'][vm.name]
45 | except Exception:
46 | return ''
47 |
48 | context = {
49 | 'perms': PermWrapper(self.context['request'].user),
50 | 'vcenter_timestamp': vcenter_timestamp,
51 | 'vcenter_resources': vcenter_resources,
52 | }
53 | context.update(csrf(self.context['request']))
54 | return self.render('netbox_vcenter/virtualmachine/vcenter_resources.html', context)
55 |
56 |
57 | template_extensions = [ClusterInfo, VirtualMachineInfo]
58 |
--------------------------------------------------------------------------------
/netbox_vcenter/templates/netbox_vcenter/cluster/vcenter_info.html:
--------------------------------------------------------------------------------
1 | {% if perms.netbox_vcenter.view_clustervcenter %}
2 |