Merge "SysInv Decoupling: Create Inventory Service"
This commit is contained in:
commit
286577940f
|
@ -23,3 +23,7 @@ pxe-network-installer
|
||||||
|
|
||||||
# platform-kickstarts
|
# platform-kickstarts
|
||||||
platform-kickstarts
|
platform-kickstarts
|
||||||
|
|
||||||
|
# inventory
|
||||||
|
inventory
|
||||||
|
python-inventoryclient
|
||||||
|
|
|
@ -5,3 +5,5 @@ mtce-control
|
||||||
mtce-storage
|
mtce-storage
|
||||||
installer/pxe-network-installer
|
installer/pxe-network-installer
|
||||||
kickstart
|
kickstart
|
||||||
|
inventory
|
||||||
|
python-inventoryclient
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
Metadata-Version: 1.1
|
||||||
|
Name: inventory
|
||||||
|
Version: 1.0
|
||||||
|
Summary: Inventory
|
||||||
|
Home-page: https://wiki.openstack.org/wiki/StarlingX
|
||||||
|
Author: StarlingX
|
||||||
|
Author-email: starlingx-discuss@lists.starlingx.io
|
||||||
|
License: Apache-2.0
|
||||||
|
|
||||||
|
Description: Inventory Service
|
||||||
|
|
||||||
|
|
||||||
|
Platform: UNKNOWN
|
|
@ -0,0 +1,2 @@
|
||||||
|
SRC_DIR="inventory"
|
||||||
|
TIS_PATCH_VER=1
|
|
@ -0,0 +1,195 @@
|
||||||
|
Summary: Inventory
|
||||||
|
Name: inventory
|
||||||
|
Version: 1.0
|
||||||
|
Release: %{tis_patch_ver}%{?_tis_dist}
|
||||||
|
License: Apache-2.0
|
||||||
|
Group: base
|
||||||
|
Packager: Wind River <info@windriver.com>
|
||||||
|
URL: unknown
|
||||||
|
Source0: %{name}-%{version}.tar.gz
|
||||||
|
|
||||||
|
BuildRequires: cgts-client
|
||||||
|
BuildRequires: python-setuptools
|
||||||
|
BuildRequires: python-jsonpatch
|
||||||
|
BuildRequires: python-keystoneauth1
|
||||||
|
BuildRequires: python-keystonemiddleware
|
||||||
|
BuildRequires: python-mock
|
||||||
|
BuildRequires: python-neutronclient
|
||||||
|
BuildRequires: python-oslo-concurrency
|
||||||
|
BuildRequires: python-oslo-config
|
||||||
|
BuildRequires: python-oslo-context
|
||||||
|
BuildRequires: python-oslo-db
|
||||||
|
BuildRequires: python-oslo-db-tests
|
||||||
|
BuildRequires: python-oslo-i18n
|
||||||
|
BuildRequires: python-oslo-log
|
||||||
|
BuildRequires: python-oslo-messaging
|
||||||
|
BuildRequires: python-oslo-middleware
|
||||||
|
BuildRequires: python-oslo-policy
|
||||||
|
BuildRequires: python-oslo-rootwrap
|
||||||
|
BuildRequires: python-oslo-serialization
|
||||||
|
BuildRequires: python-oslo-service
|
||||||
|
BuildRequires: python-oslo-utils
|
||||||
|
BuildRequires: python-oslo-versionedobjects
|
||||||
|
BuildRequires: python-oslotest
|
||||||
|
BuildRequires: python-osprofiler
|
||||||
|
BuildRequires: python-os-testr
|
||||||
|
BuildRequires: python-pbr
|
||||||
|
BuildRequires: python-pecan
|
||||||
|
BuildRequires: python-psutil
|
||||||
|
BuildRequires: python-requests
|
||||||
|
BuildRequires: python-retrying
|
||||||
|
BuildRequires: python-six
|
||||||
|
BuildRequires: python-sqlalchemy
|
||||||
|
BuildRequires: python-stevedore
|
||||||
|
BuildRequires: python-webob
|
||||||
|
BuildRequires: python-wsme
|
||||||
|
BuildRequires: systemd
|
||||||
|
BuildRequires: systemd-devel
|
||||||
|
|
||||||
|
|
||||||
|
Requires: python-pyudev
|
||||||
|
Requires: pyparted
|
||||||
|
Requires: python-ipaddr
|
||||||
|
Requires: python-paste
|
||||||
|
Requires: python-eventlet
|
||||||
|
Requires: python-futurist >= 0.11.0
|
||||||
|
Requires: python-jsonpatch
|
||||||
|
Requires: python-keystoneauth1 >= 3.1.0
|
||||||
|
Requires: python-keystonemiddleware >= 4.12.0
|
||||||
|
Requires: python-neutronclient >= 6.3.0
|
||||||
|
Requires: python-oslo-concurrency >= 3.8.0
|
||||||
|
Requires: python-oslo-config >= 2:4.0.0
|
||||||
|
Requires: python-oslo-context >= 2.14.0
|
||||||
|
Requires: python-oslo-db >= 4.24.0
|
||||||
|
Requires: python-oslo-i18n >= 2.1.0
|
||||||
|
Requires: python-oslo-log >= 3.22.0
|
||||||
|
Requires: python-oslo-messaging >= 5.24.2
|
||||||
|
Requires: python-oslo-middleware >= 3.27.0
|
||||||
|
Requires: python-oslo-policy >= 1.23.0
|
||||||
|
Requires: python-oslo-rootwrap >= 5.0.0
|
||||||
|
Requires: python-oslo-serialization >= 1.10.0
|
||||||
|
Requires: python-oslo-service >= 1.10.0
|
||||||
|
Requires: python-oslo-utils >= 3.20.0
|
||||||
|
Requires: python-oslo-versionedobjects >= 1.17.0
|
||||||
|
Requires: python-osprofiler >= 1.4.0
|
||||||
|
Requires: python-pbr
|
||||||
|
Requires: python-pecan
|
||||||
|
Requires: python-psutil
|
||||||
|
Requires: python-requests
|
||||||
|
Requires: python-retrying
|
||||||
|
Requires: python-six
|
||||||
|
Requires: python-sqlalchemy
|
||||||
|
Requires: python-stevedore >= 1.20.0
|
||||||
|
Requires: python-webob >= 1.7.1
|
||||||
|
Requires: python-wsme
|
||||||
|
|
||||||
|
%description
|
||||||
|
Inventory Service
|
||||||
|
|
||||||
|
%define local_bindir /usr/bin/
|
||||||
|
%define local_etc_goenabledd /etc/goenabled.d/
|
||||||
|
%define local_etc_inventory /etc/inventory/
|
||||||
|
%define local_etc_motdd /etc/motd.d/
|
||||||
|
%define pythonroot /usr/lib64/python2.7/site-packages
|
||||||
|
%define ocf_resourced /usr/lib/ocf/resource.d
|
||||||
|
|
||||||
|
%define local_etc_initd /etc/init.d/
|
||||||
|
%define local_etc_pmond /etc/pmon.d/
|
||||||
|
|
||||||
|
%define debug_package %{nil}
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup
|
||||||
|
|
||||||
|
# Remove bundled egg-info
|
||||||
|
rm -rf *.egg-info
|
||||||
|
|
||||||
|
%build
|
||||||
|
echo "Start inventory build"
|
||||||
|
export PBR_VERSION=%{version}
|
||||||
|
%{__python} setup.py build
|
||||||
|
PYTHONPATH=. oslo-config-generator --config-file=inventory/config-generator.conf
|
||||||
|
|
||||||
|
%install
|
||||||
|
echo "Start inventory install"
|
||||||
|
export PBR_VERSION=%{version}
|
||||||
|
%{__python} setup.py install --root=%{buildroot} \
|
||||||
|
--install-lib=%{pythonroot} \
|
||||||
|
--prefix=/usr \
|
||||||
|
--install-data=/usr/share \
|
||||||
|
--single-version-externally-managed
|
||||||
|
|
||||||
|
install -d -m 755 %{buildroot}%{local_etc_goenabledd}
|
||||||
|
install -p -D -m 755 etc/inventory/inventory_goenabled_check.sh %{buildroot}%{local_etc_goenabledd}/inventory_goenabled_check.sh
|
||||||
|
|
||||||
|
install -d -m 755 %{buildroot}%{local_etc_inventory}
|
||||||
|
install -p -D -m 755 etc/inventory/policy.json %{buildroot}%{local_etc_inventory}/policy.json
|
||||||
|
|
||||||
|
install -d -m 755 %{buildroot}%{local_etc_motdd}
|
||||||
|
install -p -D -m 755 etc/inventory/motd-system %{buildroot}%{local_etc_motdd}/10-system-config
|
||||||
|
|
||||||
|
install -m 755 -p -D scripts/inventory-api %{buildroot}/usr/lib/ocf/resource.d/platform/inventory-api
|
||||||
|
install -m 755 -p -D scripts/inventory-conductor %{buildroot}/usr/lib/ocf/resource.d/platform/inventory-conductor
|
||||||
|
|
||||||
|
install -m 644 -p -D scripts/inventory-api.service %{buildroot}%{_unitdir}/inventory-api.service
|
||||||
|
install -m 644 -p -D scripts/inventory-conductor.service %{buildroot}%{_unitdir}/inventory-conductor.service
|
||||||
|
|
||||||
|
# TODO(jkung) activate inventory-agent with puppet integration)
|
||||||
|
# install -d -m 755 %{buildroot}%{local_etc_initd}
|
||||||
|
# install -p -D -m 755 scripts/inventory-agent-initd %{buildroot}%{local_etc_initd}/inventory-agent
|
||||||
|
|
||||||
|
# install -d -m 755 %{buildroot}%{local_etc_pmond}
|
||||||
|
# install -p -D -m 644 etc/inventory/inventory-agent-pmond.conf %{buildroot}%{local_etc_pmond}/inventory-agent-pmond.conf
|
||||||
|
# install -p -D -m 644 scripts/inventory-agent.service %{buildroot}%{_unitdir}/inventory-agent.service
|
||||||
|
|
||||||
|
# Install sql migration
|
||||||
|
install -m 644 inventory/db/sqlalchemy/migrate_repo/migrate.cfg %{buildroot}%{pythonroot}/inventory/db/sqlalchemy/migrate_repo/migrate.cfg
|
||||||
|
|
||||||
|
# install default config files
|
||||||
|
cd %{_builddir}/%{name}-%{version} && oslo-config-generator --config-file inventory/config-generator.conf --output-file %{_builddir}/%{name}-%{version}/inventory.conf.sample
|
||||||
|
# install -p -D -m 644 %{_builddir}/%{name}-%{version}/inventory.conf.sample %{buildroot}%{_sysconfdir}/inventory/inventory.conf
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(jkung) activate inventory-agent
|
||||||
|
# %post
|
||||||
|
# /usr/bin/systemctl enable inventory-agent.service >/dev/null 2>&1
|
||||||
|
|
||||||
|
|
||||||
|
%clean
|
||||||
|
echo "CLEAN CALLED"
|
||||||
|
rm -rf $RPM_BUILD_ROOT
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(-,root,root,-)
|
||||||
|
%doc LICENSE
|
||||||
|
|
||||||
|
%{local_bindir}/*
|
||||||
|
|
||||||
|
%{pythonroot}/%{name}
|
||||||
|
|
||||||
|
%{pythonroot}/%{name}-%{version}*.egg-info
|
||||||
|
|
||||||
|
%{local_etc_goenabledd}/*
|
||||||
|
|
||||||
|
%{local_etc_inventory}/*
|
||||||
|
|
||||||
|
%{local_etc_motdd}/*
|
||||||
|
|
||||||
|
# SM OCF Start/Stop/Monitor Scripts
|
||||||
|
%{ocf_resourced}/platform/inventory-api
|
||||||
|
%{ocf_resourced}/platform/inventory-conductor
|
||||||
|
|
||||||
|
# systemctl service files
|
||||||
|
%{_unitdir}/inventory-api.service
|
||||||
|
%{_unitdir}/inventory-conductor.service
|
||||||
|
|
||||||
|
# %{_bindir}/inventory-agent
|
||||||
|
%{_bindir}/inventory-api
|
||||||
|
%{_bindir}/inventory-conductor
|
||||||
|
%{_bindir}/inventory-dbsync
|
||||||
|
%{_bindir}/inventory-dnsmasq-lease-update
|
||||||
|
|
||||||
|
# inventory-agent files
|
||||||
|
# %{local_etc_initd}/inventory-agent
|
||||||
|
# %{local_etc_pmond}/inventory-agent-pmond.conf
|
||||||
|
# %{_unitdir}/inventory-agent.service
|
|
@ -0,0 +1,6 @@
|
||||||
|
[run]
|
||||||
|
branch = True
|
||||||
|
source = inventory
|
||||||
|
|
||||||
|
[report]
|
||||||
|
ignore_errors = True
|
|
@ -0,0 +1,59 @@
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Packages
|
||||||
|
*.egg*
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
eggs
|
||||||
|
parts
|
||||||
|
bin
|
||||||
|
var
|
||||||
|
sdist
|
||||||
|
develop-eggs
|
||||||
|
.installed.cfg
|
||||||
|
lib
|
||||||
|
lib64
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
cover/
|
||||||
|
.coverage*
|
||||||
|
!.coveragerc
|
||||||
|
.tox
|
||||||
|
nosetests.xml
|
||||||
|
.testrepository
|
||||||
|
.stestr
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Mr Developer
|
||||||
|
.mr.developer.cfg
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
|
||||||
|
# Complexity
|
||||||
|
output/*.html
|
||||||
|
output/*/index.html
|
||||||
|
|
||||||
|
# Sphinx
|
||||||
|
doc/build
|
||||||
|
|
||||||
|
# pbr generates these
|
||||||
|
AUTHORS
|
||||||
|
ChangeLog
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
*~
|
||||||
|
.*.swp
|
||||||
|
.*sw?
|
||||||
|
|
||||||
|
# Files created by releasenotes build
|
||||||
|
releasenotes/build
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Format is:
|
||||||
|
# <preferred e-mail> <other e-mail 1>
|
||||||
|
# <preferred e-mail> <other e-mail 2>
|
|
@ -0,0 +1,3 @@
|
||||||
|
[DEFAULT]
|
||||||
|
test_path=./inventory/tests
|
||||||
|
top_dir=./
|
|
@ -0,0 +1,19 @@
|
||||||
|
If you would like to contribute to the development of StarlingX, you must
|
||||||
|
follow the steps in this page:
|
||||||
|
|
||||||
|
https://wiki.openstack.org/wiki/StarlingX/Contribution_Guidelines
|
||||||
|
|
||||||
|
If you already have a good understanding of how the system works and your
|
||||||
|
StarlingX accounts are set up, you can skip to the development workflow
|
||||||
|
section of this documentation to learn how changes to StarlingX should be
|
||||||
|
submitted for review via the Gerrit tool:
|
||||||
|
|
||||||
|
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||||
|
|
||||||
|
Pull requests submitted through GitHub will be ignored.
|
||||||
|
|
||||||
|
Bugs should be filed on Launchpad:
|
||||||
|
https://bugs.launchpad.net/starlingx
|
||||||
|
|
||||||
|
Storyboard:
|
||||||
|
https://storyboard.openstack.org/#!/story/2002950
|
|
@ -0,0 +1,4 @@
|
||||||
|
inventory Style Commandments
|
||||||
|
============================
|
||||||
|
|
||||||
|
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/
|
|
@ -0,0 +1,176 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Placeholder to allow setup.py to work.
|
||||||
|
Removing this requires modifying the
|
||||||
|
setup.py manifest.
|
|
@ -0,0 +1,2 @@
|
||||||
|
[python: **.py]
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
|
||||||
|
openstackdocstheme>=1.18.1 # Apache-2.0
|
||||||
|
# releasenotes
|
||||||
|
reno>=2.5.0 # Apache-2.0
|
|
@ -0,0 +1,5 @@
|
||||||
|
====================
|
||||||
|
Administrators guide
|
||||||
|
====================
|
||||||
|
|
||||||
|
Administrators guide of inventory.
|
|
@ -0,0 +1,5 @@
|
||||||
|
================================
|
||||||
|
Command line interface reference
|
||||||
|
================================
|
||||||
|
|
||||||
|
CLI reference of inventory.
|
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
# -- General configuration ----------------------------------------------------
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'openstackdocstheme',
|
||||||
|
#'sphinx.ext.intersphinx',
|
||||||
|
]
|
||||||
|
|
||||||
|
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
||||||
|
# text edit cycles.
|
||||||
|
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'inventory'
|
||||||
|
copyright = u'2018, StarlingX'
|
||||||
|
|
||||||
|
# openstackdocstheme options
|
||||||
|
repository_name = 'stx-metal'
|
||||||
|
bug_project = '22952'
|
||||||
|
bug_tag = ''
|
||||||
|
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
add_module_names = True
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# -- Options for HTML output --------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||||
|
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||||
|
# html_theme_path = ["."]
|
||||||
|
# html_theme = '_theme'
|
||||||
|
# html_static_path = ['static']
|
||||||
|
html_theme = 'starlingxdocs'
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = '%sdoc' % project
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass
|
||||||
|
# [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
('index',
|
||||||
|
'%s.tex' % project,
|
||||||
|
u'%s Documentation' % project,
|
||||||
|
u'OpenStack Developers', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
|
#intersphinx_mapping = {'http://docs.python.org/': None}
|
|
@ -0,0 +1,5 @@
|
||||||
|
=============
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Configuration of inventory.
|
|
@ -0,0 +1,4 @@
|
||||||
|
============
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
.. include:: ../../../CONTRIBUTING.rst
|
|
@ -0,0 +1,9 @@
|
||||||
|
=========================
|
||||||
|
Contributor Documentation
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
contributing
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
.. inventory documentation master file, created by
|
||||||
|
sphinx-quickstart on Tue Jul 9 22:26:36 2013.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
=========================================
|
||||||
|
Welcome to the documentation of inventory
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
readme
|
||||||
|
install/index
|
||||||
|
library/index
|
||||||
|
contributor/index
|
||||||
|
configuration/index
|
||||||
|
cli/index
|
||||||
|
user/index
|
||||||
|
admin/index
|
||||||
|
reference/index
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
|
@ -0,0 +1,10 @@
|
||||||
|
2. Edit the ``/etc/inventory/inventory.conf`` file and complete the following
|
||||||
|
actions:
|
||||||
|
|
||||||
|
* In the ``[database]`` section, configure database access:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[database]
|
||||||
|
...
|
||||||
|
connection = mysql+pymysql://inventory:INVENTORY_DBPASS@controller/inventory
|
|
@ -0,0 +1,75 @@
|
||||||
|
Prerequisites
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Before you install and configure the inventory service,
|
||||||
|
you must create a database, service credentials, and API endpoints.
|
||||||
|
|
||||||
|
#. To create the database, complete these steps:
|
||||||
|
|
||||||
|
* Use the database access client to connect to the database
|
||||||
|
server as the ``root`` user:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ mysql -u root -p
|
||||||
|
|
||||||
|
* Create the ``inventory`` database:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
CREATE DATABASE inventory;
|
||||||
|
|
||||||
|
* Grant proper access to the ``inventory`` database:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON inventory.* TO 'inventory'@'localhost' \
|
||||||
|
IDENTIFIED BY 'INVENTORY_DBPASS';
|
||||||
|
GRANT ALL PRIVILEGES ON inventory.* TO 'inventory'@'%' \
|
||||||
|
IDENTIFIED BY 'INVENTORY_DBPASS';
|
||||||
|
|
||||||
|
Replace ``INVENTORY_DBPASS`` with a suitable password.
|
||||||
|
|
||||||
|
* Exit the database access client.
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
exit;
|
||||||
|
|
||||||
|
#. Source the ``admin`` credentials to gain access to
|
||||||
|
admin-only CLI commands:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ . admin-openrc
|
||||||
|
|
||||||
|
#. To create the service credentials, complete these steps:
|
||||||
|
|
||||||
|
* Create the ``inventory`` user:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ openstack user create --domain default --password-prompt inventory
|
||||||
|
|
||||||
|
* Add the ``admin`` role to the ``inventory`` user:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ openstack role add --project service --user inventory admin
|
||||||
|
|
||||||
|
* Create the inventory service entities:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ openstack service create --name inventory --description "inventory" inventory
|
||||||
|
|
||||||
|
#. Create the inventory service API endpoints:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ openstack endpoint create --region RegionOne \
|
||||||
|
inventory public http://controller:XXXX/vY/%\(tenant_id\)s
|
||||||
|
$ openstack endpoint create --region RegionOne \
|
||||||
|
inventory internal http://controller:XXXX/vY/%\(tenant_id\)s
|
||||||
|
$ openstack endpoint create --region RegionOne \
|
||||||
|
inventory admin http://controller:XXXX/vY/%\(tenant_id\)s
|
|
@ -0,0 +1,9 @@
|
||||||
|
==========================
|
||||||
|
inventory service overview
|
||||||
|
==========================
|
||||||
|
The inventory service provides host inventory of resources on the host.
|
||||||
|
|
||||||
|
The inventory service consists of the following components:
|
||||||
|
|
||||||
|
``inventory-api`` service
|
||||||
|
Accepts and responds to end user API calls...
|
|
@ -0,0 +1,17 @@
|
||||||
|
====================================
|
||||||
|
inventory service installation guide
|
||||||
|
====================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
get_started.rst
|
||||||
|
install.rst
|
||||||
|
verify.rst
|
||||||
|
next-steps.rst
|
||||||
|
|
||||||
|
The inventory service (inventory) provides...
|
||||||
|
|
||||||
|
This chapter assumes a working setup of StarlingX following the
|
||||||
|
`StarlingX Installation Guide
|
||||||
|
<https://docs.starlingx.io/installation_guide/index.html>`_.
|
|
@ -0,0 +1,34 @@
|
||||||
|
.. _install-obs:
|
||||||
|
|
||||||
|
|
||||||
|
Install and configure for openSUSE and SUSE Linux Enterprise
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This section describes how to install and configure the inventory service
|
||||||
|
for openSUSE Leap 42.1 and SUSE Linux Enterprise Server 12 SP1.
|
||||||
|
|
||||||
|
.. include:: common_prerequisites.rst
|
||||||
|
|
||||||
|
Install and configure components
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
#. Install the packages:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# zypper --quiet --non-interactive install
|
||||||
|
|
||||||
|
.. include:: common_configure.rst
|
||||||
|
|
||||||
|
|
||||||
|
Finalize installation
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Start the inventory services and configure them to start when
|
||||||
|
the system boots:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# systemctl enable openstack-inventory-api.service
|
||||||
|
|
||||||
|
# systemctl start openstack-inventory-api.service
|
|
@ -0,0 +1,33 @@
|
||||||
|
.. _install-rdo:
|
||||||
|
|
||||||
|
Install and configure for Red Hat Enterprise Linux and CentOS
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
This section describes how to install and configure the inventory service
|
||||||
|
for Red Hat Enterprise Linux 7 and CentOS 7.
|
||||||
|
|
||||||
|
.. include:: common_prerequisites.rst
|
||||||
|
|
||||||
|
Install and configure components
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
#. Install the packages:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# yum install
|
||||||
|
|
||||||
|
.. include:: common_configure.rst
|
||||||
|
|
||||||
|
Finalize installation
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Start the inventory services and configure them to start when
|
||||||
|
the system boots:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# systemctl enable openstack-inventory-api.service
|
||||||
|
|
||||||
|
# systemctl start openstack-inventory-api.service
|
|
@ -0,0 +1,31 @@
|
||||||
|
.. _install-ubuntu:
|
||||||
|
|
||||||
|
Install and configure for Ubuntu
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This section describes how to install and configure the inventory
|
||||||
|
service for Ubuntu 14.04 (LTS).
|
||||||
|
|
||||||
|
.. include:: common_prerequisites.rst
|
||||||
|
|
||||||
|
Install and configure components
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
#. Install the packages:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# apt-get update
|
||||||
|
|
||||||
|
# apt-get install
|
||||||
|
|
||||||
|
.. include:: common_configure.rst
|
||||||
|
|
||||||
|
Finalize installation
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Restart the inventory services:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# service openstack-inventory-api restart
|
|
@ -0,0 +1,20 @@
|
||||||
|
.. _install:
|
||||||
|
|
||||||
|
Install and configure
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This section describes how to install and configure the
|
||||||
|
inventory service, code-named inventory, on the controller node.
|
||||||
|
|
||||||
|
This section assumes that you already have a working OpenStack
|
||||||
|
environment with at least the following components installed:
|
||||||
|
.. (add the appropriate services here and further notes)
|
||||||
|
|
||||||
|
Note that installation and configuration vary by distribution.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
install-obs.rst
|
||||||
|
install-rdo.rst
|
||||||
|
install-ubuntu.rst
|
|
@ -0,0 +1,9 @@
|
||||||
|
.. _next-steps:
|
||||||
|
|
||||||
|
Next steps
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
Your OpenStack environment now includes the inventory service.
|
||||||
|
|
||||||
|
To add additional services, see
|
||||||
|
https://docs.openstack.org/project-install-guide/ocata/.
|
|
@ -0,0 +1,24 @@
|
||||||
|
.. _verify:
|
||||||
|
|
||||||
|
Verify operation
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Verify operation of the inventory service.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Perform these commands on the controller node.
|
||||||
|
|
||||||
|
#. Source the ``admin`` project credentials to gain access to
|
||||||
|
admin-only CLI commands:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ . admin-openrc
|
||||||
|
|
||||||
|
#. List service components to verify successful launch and registration
|
||||||
|
of each process:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ openstack inventory service list
|
|
@ -0,0 +1,7 @@
|
||||||
|
=====
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
To use inventory in a project:
|
||||||
|
|
||||||
|
import inventory
|
|
@ -0,0 +1 @@
|
||||||
|
.. include:: ../../README.rst
|
|
@ -0,0 +1,5 @@
|
||||||
|
==========
|
||||||
|
References
|
||||||
|
==========
|
||||||
|
|
||||||
|
References of inventory.
|
|
@ -0,0 +1,5 @@
|
||||||
|
===========
|
||||||
|
Users guide
|
||||||
|
===========
|
||||||
|
|
||||||
|
Users guide of inventory.
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (c) 2015-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# This script removes a load from a controller.
|
||||||
|
# The load version is passed in as the first variable.
|
||||||
|
|
||||||
|
: ${1?"Usage $0 VERSION"}
|
||||||
|
VERSION=$1
|
||||||
|
|
||||||
|
FEED_DIR=/www/pages/feed/rel-$VERSION
|
||||||
|
|
||||||
|
rm -f /pxeboot/pxelinux.cfg.files/*-$VERSION
|
||||||
|
rm -rf /pxeboot/rel-$VERSION
|
||||||
|
|
||||||
|
rm -f /usr/sbin/pxeboot-update-$VERSION.sh
|
||||||
|
|
||||||
|
rm -rf $FEED_DIR
|
|
@ -0,0 +1,9 @@
|
||||||
|
[process]
|
||||||
|
process = inventory-agent
|
||||||
|
pidfile = /var/run/inventory-agent.pid
|
||||||
|
script = /etc/init.d/inventory-agent
|
||||||
|
style = lsb ; ocf or lsb
|
||||||
|
severity = major ; minor, major, critical
|
||||||
|
restarts = 3 ; restarts before error assertion
|
||||||
|
interval = 5 ; number of seconds to wait between restarts
|
||||||
|
debounce = 20 ; number of seconds to wait before degrade clear
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# Inventory "goenabled" check.
|
||||||
|
# Wait for inventory information to be posted prior to allowing goenabled.
|
||||||
|
|
||||||
|
NAME=$(basename $0)
|
||||||
|
INVENTORY_READY_FLAG=/var/run/.inventory_ready
|
||||||
|
|
||||||
|
# logfile=/var/log/platform.log
|
||||||
|
|
||||||
|
function LOG {
|
||||||
|
logger "$NAME: $*"
|
||||||
|
# echo "`date "+%FT%T"`: $NAME: $*" >> $logfile
|
||||||
|
}
|
||||||
|
|
||||||
|
count=0
|
||||||
|
while [ $count -le 45 ]; do
|
||||||
|
if [ -f $INVENTORY_READY_FLAG ]; then
|
||||||
|
LOG "Inventory is ready. Passing goenabled check."
|
||||||
|
echo "Inventory goenabled iterations PASS $count"
|
||||||
|
LOG "Inventory goenabled iterations PASS $count"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
count=$(($count+1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Inventory goenabled iterations FAIL $count"
|
||||||
|
|
||||||
|
LOG "Inventory is not ready. Continue."
|
||||||
|
exit 0
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# update inventory MOTD if motd.system content present
|
||||||
|
|
||||||
|
[ -f /etc/inventory/motd.system ] && cat /etc/inventory/motd.system || true
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"admin": "role:admin or role:administrator",
|
||||||
|
"admin_api": "is_admin:True",
|
||||||
|
"default": "rule:admin_api"
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import pbr.version
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = pbr.version.VersionInfo(
|
||||||
|
'inventory').version_string()
|
|
@ -0,0 +1,114 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
"""Base agent manager functionality."""
|
||||||
|
|
||||||
|
import futurist
|
||||||
|
from futurist import periodics
|
||||||
|
from futurist import rejection
|
||||||
|
import inspect
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAgentManager(object):
|
||||||
|
|
||||||
|
def __init__(self, host, topic):
|
||||||
|
super(BaseAgentManager, self).__init__()
|
||||||
|
if not host:
|
||||||
|
host = cfg.CONF.host
|
||||||
|
self.host = host
|
||||||
|
self.topic = topic
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
def init_host(self, admin_context=None):
|
||||||
|
"""Initialize the agent host.
|
||||||
|
|
||||||
|
:param admin_context: the admin context to pass to periodic tasks.
|
||||||
|
:raises: RuntimeError when agent is already running.
|
||||||
|
"""
|
||||||
|
if self._started:
|
||||||
|
raise RuntimeError(_('Attempt to start an already running '
|
||||||
|
'agent manager'))
|
||||||
|
|
||||||
|
rejection_func = rejection.reject_when_reached(64)
|
||||||
|
# CONF.conductor.workers_pool_size)
|
||||||
|
self._executor = futurist.GreenThreadPoolExecutor(
|
||||||
|
64, check_and_reject=rejection_func)
|
||||||
|
# JK max_workers=CONF.conductor.workers_pool_size,
|
||||||
|
"""Executor for performing tasks async."""
|
||||||
|
|
||||||
|
# Collect driver-specific periodic tasks.
|
||||||
|
# Conductor periodic tasks accept context argument,
|
||||||
|
LOG.info('Collecting periodic tasks')
|
||||||
|
self._periodic_task_callables = []
|
||||||
|
self._collect_periodic_tasks(self, (admin_context,))
|
||||||
|
|
||||||
|
self._periodic_tasks = periodics.PeriodicWorker(
|
||||||
|
self._periodic_task_callables,
|
||||||
|
executor_factory=periodics.ExistingExecutor(self._executor))
|
||||||
|
|
||||||
|
# Start periodic tasks
|
||||||
|
self._periodic_tasks_worker = self._executor.submit(
|
||||||
|
self._periodic_tasks.start, allow_empty=True)
|
||||||
|
self._periodic_tasks_worker.add_done_callback(
|
||||||
|
self._on_periodic_tasks_stop)
|
||||||
|
|
||||||
|
self._started = True
|
||||||
|
|
||||||
|
def del_host(self, deregister=True):
|
||||||
|
# Conductor deregistration fails if called on non-initialized
|
||||||
|
# agent (e.g. when rpc server is unreachable).
|
||||||
|
if not hasattr(self, 'agent'):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._periodic_tasks.stop()
|
||||||
|
self._periodic_tasks.wait()
|
||||||
|
self._executor.shutdown(wait=True)
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
def _collect_periodic_tasks(self, obj, args):
|
||||||
|
"""Collect periodic tasks from a given object.
|
||||||
|
|
||||||
|
Populates self._periodic_task_callables with tuples
|
||||||
|
(callable, args, kwargs).
|
||||||
|
|
||||||
|
:param obj: object containing periodic tasks as methods
|
||||||
|
:param args: tuple with arguments to pass to every task
|
||||||
|
"""
|
||||||
|
for name, member in inspect.getmembers(obj):
|
||||||
|
if periodics.is_periodic(member):
|
||||||
|
LOG.debug('Found periodic task %(owner)s.%(member)s',
|
||||||
|
{'owner': obj.__class__.__name__,
|
||||||
|
'member': name})
|
||||||
|
self._periodic_task_callables.append((member, args, {}))
|
||||||
|
|
||||||
|
def _on_periodic_tasks_stop(self, fut):
|
||||||
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.critical('Periodic tasks worker has failed: %s', exc)
|
||||||
|
else:
|
||||||
|
LOG.info('Successfully shut down periodic tasks')
|
||||||
|
|
||||||
|
def _spawn_worker(self, func, *args, **kwargs):
|
||||||
|
|
||||||
|
"""Create a greenthread to run func(*args, **kwargs).
|
||||||
|
|
||||||
|
Spawns a greenthread if there are free slots in pool, otherwise raises
|
||||||
|
exception. Execution control returns immediately to the caller.
|
||||||
|
|
||||||
|
:returns: Future object.
|
||||||
|
:raises: NoFreeConductorWorker if worker pool is currently full.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._executor.submit(func, *args, **kwargs)
|
||||||
|
except futurist.RejectedSubmission:
|
||||||
|
raise exception.NoFreeConductorWorker()
|
|
@ -0,0 +1,369 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
""" inventory idisk Utilities and helper functions."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pyudev
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from inventory.common import constants
|
||||||
|
from inventory.common import context
|
||||||
|
from inventory.common import utils
|
||||||
|
from inventory.conductor import rpcapi as conductor_rpcapi
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DiskOperator(object):
|
||||||
|
'''Class to encapsulate Disk operations for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.num_cpus = 0
|
||||||
|
self.num_nodes = 0
|
||||||
|
self.float_cpuset = 0
|
||||||
|
self.default_hugepage_size_kB = 0
|
||||||
|
self.total_memory_MiB = 0
|
||||||
|
self.free_memory_MiB = 0
|
||||||
|
self.total_memory_nodes_MiB = []
|
||||||
|
self.free_memory_nodes_MiB = []
|
||||||
|
self.topology = {}
|
||||||
|
|
||||||
|
def convert_range_string_to_list(self, s):
|
||||||
|
olist = []
|
||||||
|
s = s.strip()
|
||||||
|
if s:
|
||||||
|
for part in s.split(','):
|
||||||
|
if '-' in part:
|
||||||
|
a, b = part.split('-')
|
||||||
|
a, b = int(a), int(b)
|
||||||
|
olist.extend(range(a, b + 1))
|
||||||
|
else:
|
||||||
|
a = int(part)
|
||||||
|
olist.append(a)
|
||||||
|
olist.sort()
|
||||||
|
return olist
|
||||||
|
|
||||||
|
def get_rootfs_node(self):
|
||||||
|
cmdline_file = '/proc/cmdline'
|
||||||
|
device = None
|
||||||
|
|
||||||
|
with open(cmdline_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
for param in line.split():
|
||||||
|
params = param.split("=", 1)
|
||||||
|
if params[0] == "root":
|
||||||
|
if "UUID=" in params[1]:
|
||||||
|
key, uuid = params[1].split("=")
|
||||||
|
symlink = "/dev/disk/by-uuid/%s" % uuid
|
||||||
|
device = os.path.basename(os.readlink(symlink))
|
||||||
|
else:
|
||||||
|
device = os.path.basename(params[1])
|
||||||
|
|
||||||
|
if device is not None:
|
||||||
|
if constants.DEVICE_NAME_NVME in device:
|
||||||
|
re_line = re.compile(r'^(nvme[0-9]*n[0-9]*)')
|
||||||
|
else:
|
||||||
|
re_line = re.compile(r'^(\D*)')
|
||||||
|
match = re_line.search(device)
|
||||||
|
if match:
|
||||||
|
return os.path.join("/dev", match.group(1))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@utils.skip_udev_partition_probe
|
||||||
|
def get_disk_available_mib(self, device_node):
|
||||||
|
# Check that partition table format is GPT.
|
||||||
|
# Return 0 if not.
|
||||||
|
if not utils.disk_is_gpt(device_node=device_node):
|
||||||
|
LOG.debug("Format of disk node %s is not GPT." % device_node)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
pvs_command = '{} {}'.format('pvs | grep -w ', device_node)
|
||||||
|
pvs_process = subprocess.Popen(pvs_command, stdout=subprocess.PIPE,
|
||||||
|
shell=True)
|
||||||
|
pvs_output = pvs_process.stdout.read()
|
||||||
|
|
||||||
|
if pvs_output:
|
||||||
|
LOG.debug("Disk %s is completely used by a PV => 0 available mib."
|
||||||
|
% device_node)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Get sector size command.
|
||||||
|
sector_size_bytes_cmd = '{} {}'.format('blockdev --getss', device_node)
|
||||||
|
|
||||||
|
# Get total free space in sectors command.
|
||||||
|
avail_space_sectors_cmd = '{} {} {}'.format(
|
||||||
|
'sgdisk -p', device_node, "| grep \"Total free space\"")
|
||||||
|
|
||||||
|
# Get the sector size.
|
||||||
|
sector_size_bytes_process = subprocess.Popen(
|
||||||
|
sector_size_bytes_cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
sector_size_bytes = sector_size_bytes_process.stdout.read().rstrip()
|
||||||
|
|
||||||
|
# Get the free space.
|
||||||
|
avail_space_sectors_process = subprocess.Popen(
|
||||||
|
avail_space_sectors_cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
avail_space_sectors_output = avail_space_sectors_process.stdout.read()
|
||||||
|
avail_space_sectors = re.findall(
|
||||||
|
'\d+', avail_space_sectors_output)[0].rstrip()
|
||||||
|
|
||||||
|
# Free space in MiB.
|
||||||
|
avail_space_mib = (int(sector_size_bytes) * int(avail_space_sectors) /
|
||||||
|
(1024 ** 2))
|
||||||
|
|
||||||
|
# Keep 2 MiB for partition table.
|
||||||
|
if avail_space_mib >= 2:
|
||||||
|
avail_space_mib = avail_space_mib - 2
|
||||||
|
else:
|
||||||
|
avail_space_mib = 0
|
||||||
|
|
||||||
|
return avail_space_mib
|
||||||
|
|
||||||
|
def disk_format_gpt(self, host_uuid, idisk_dict, is_cinder_device):
|
||||||
|
disk_node = idisk_dict.get('device_path')
|
||||||
|
|
||||||
|
utils.disk_wipe(disk_node)
|
||||||
|
utils.execute('parted', disk_node, 'mklabel', 'gpt')
|
||||||
|
|
||||||
|
if is_cinder_device:
|
||||||
|
LOG.debug("Removing .node_cinder_lvm_config_complete_file")
|
||||||
|
try:
|
||||||
|
os.remove(constants.NODE_CINDER_LVM_CONFIG_COMPLETE_FILE)
|
||||||
|
except OSError:
|
||||||
|
LOG.error(".node_cinder_lvm_config_complete_file not present.")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# On SX ensure wipe succeeds before DB is updated.
|
||||||
|
# Flag file is used to mark wiping in progress.
|
||||||
|
try:
|
||||||
|
os.remove(constants.DISK_WIPE_IN_PROGRESS_FLAG)
|
||||||
|
except OSError:
|
||||||
|
# it's ok if file is not present.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# We need to send the updated info about the host disks back to
|
||||||
|
# the conductor.
|
||||||
|
idisk_update = self.idisk_get()
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
rpcapi = conductor_rpcapi.ConductorAPI(
|
||||||
|
topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
rpcapi.idisk_update_by_ihost(ctxt,
|
||||||
|
host_uuid,
|
||||||
|
idisk_update)
|
||||||
|
|
||||||
|
def handle_exception(self, e):
|
||||||
|
traceback = sys.exc_info()[-1]
|
||||||
|
LOG.error("%s @ %s:%s" % (
|
||||||
|
e, traceback.tb_frame.f_code.co_filename, traceback.tb_lineno))
|
||||||
|
|
||||||
|
def is_rotational(self, device_name):
|
||||||
|
"""Find out if a certain disk is rotational or not. Mostly used for
|
||||||
|
determining if disk is HDD or SSD.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Obtain the path to the rotational file for the current device.
|
||||||
|
device = device_name['DEVNAME'].split('/')[-1]
|
||||||
|
rotational_path = "/sys/block/{device}/queue/rotational"\
|
||||||
|
.format(device=device)
|
||||||
|
|
||||||
|
rotational = None
|
||||||
|
# Read file and remove trailing whitespaces.
|
||||||
|
if os.path.isfile(rotational_path):
|
||||||
|
with open(rotational_path, 'r') as rot_file:
|
||||||
|
rotational = rot_file.read()
|
||||||
|
rotational = rotational.rstrip()
|
||||||
|
|
||||||
|
return rotational
|
||||||
|
|
||||||
|
def get_device_id_wwn(self, device):
|
||||||
|
"""Determine the ID and WWN of a disk from the value of the DEVLINKS
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
Note: This data is not currently being used for anything. We are
|
||||||
|
gathering this information so conductor can store for future use.
|
||||||
|
"""
|
||||||
|
# The ID and WWN default to None.
|
||||||
|
device_id = None
|
||||||
|
device_wwn = None
|
||||||
|
|
||||||
|
# If there is no DEVLINKS attribute, return None.
|
||||||
|
if 'DEVLINKS' not in device:
|
||||||
|
return device_id, device_wwn
|
||||||
|
|
||||||
|
# Extract the ID and the WWN.
|
||||||
|
LOG.debug("[DiskEnum] get_device_id_wwn: devlinks= %s" %
|
||||||
|
device['DEVLINKS'])
|
||||||
|
devlinks = device['DEVLINKS'].split()
|
||||||
|
for devlink in devlinks:
|
||||||
|
if "by-id" in devlink:
|
||||||
|
if "wwn" not in devlink:
|
||||||
|
device_id = devlink.split('/')[-1]
|
||||||
|
LOG.debug("[DiskEnum] by-id: %s id: %s" % (devlink,
|
||||||
|
device_id))
|
||||||
|
else:
|
||||||
|
device_wwn = devlink.split('/')[-1]
|
||||||
|
LOG.debug("[DiskEnum] by-wwn: %s wwn: %s" % (devlink,
|
||||||
|
device_wwn))
|
||||||
|
|
||||||
|
return device_id, device_wwn
|
||||||
|
|
||||||
|
def idisk_get(self):
|
||||||
|
"""Enumerate disk topology based on:
|
||||||
|
|
||||||
|
:param self
|
||||||
|
:returns list of disk and attributes
|
||||||
|
"""
|
||||||
|
idisk = []
|
||||||
|
context = pyudev.Context()
|
||||||
|
|
||||||
|
for device in context.list_devices(DEVTYPE='disk'):
|
||||||
|
if not utils.is_system_usable_block_device(device):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if device['MAJOR'] in constants.VALID_MAJOR_LIST:
|
||||||
|
if 'ID_PATH' in device:
|
||||||
|
device_path = "/dev/disk/by-path/" + device['ID_PATH']
|
||||||
|
LOG.debug("[DiskEnum] device_path: %s ", device_path)
|
||||||
|
else:
|
||||||
|
# We should always have a udev supplied /dev/disk/by-path
|
||||||
|
# value as a matter of normal operation. We do not expect
|
||||||
|
# this to occur, thus the error.
|
||||||
|
#
|
||||||
|
# The kickstart files for the host install require the
|
||||||
|
# by-path value also to be present or the host install will
|
||||||
|
# fail. Since the installer and the runtime share the same
|
||||||
|
# kernel/udev we should not see this message on an
|
||||||
|
# installed system.
|
||||||
|
device_path = None
|
||||||
|
LOG.error("Device %s does not have an ID_PATH value "
|
||||||
|
"provided by udev" % device.device_node)
|
||||||
|
|
||||||
|
size_mib = 0
|
||||||
|
available_mib = 0
|
||||||
|
model_num = ''
|
||||||
|
serial_id = ''
|
||||||
|
|
||||||
|
# Can merge all try/except in one block but this allows
|
||||||
|
# at least attributes with no exception to be filled
|
||||||
|
try:
|
||||||
|
size_mib = utils.get_disk_capacity_mib(device.device_node)
|
||||||
|
except Exception as e:
|
||||||
|
self.handle_exception("Could not retrieve disk size - %s "
|
||||||
|
% e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
available_mib = self.get_disk_available_mib(
|
||||||
|
device_node=device.device_node)
|
||||||
|
except Exception as e:
|
||||||
|
self.handle_exception(
|
||||||
|
"Could not retrieve disk %s free space" % e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ID_MODEL received from udev is not correct for disks that
|
||||||
|
# are used entirely for LVM. LVM replaced the model ID with
|
||||||
|
# its own identifier that starts with "LVM PV".For this
|
||||||
|
# reason we will attempt to retrieve the correct model ID
|
||||||
|
# by using 2 different commands: hdparm and lsblk and
|
||||||
|
# hdparm. If one of them fails, the other one can attempt
|
||||||
|
# to retrieve the information. Else we use udev.
|
||||||
|
|
||||||
|
# try hdparm command first
|
||||||
|
hdparm_command = 'hdparm -I %s |grep Model' % (
|
||||||
|
device.get('DEVNAME'))
|
||||||
|
hdparm_process = subprocess.Popen(
|
||||||
|
hdparm_command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
shell=True)
|
||||||
|
hdparm_output = hdparm_process.communicate()[0]
|
||||||
|
if hdparm_process.returncode == 0:
|
||||||
|
second_half = hdparm_output.split(':')[1]
|
||||||
|
model_num = second_half.strip()
|
||||||
|
else:
|
||||||
|
# try lsblk command
|
||||||
|
lsblk_command = 'lsblk -dn --output MODEL %s' % (
|
||||||
|
device.get('DEVNAME'))
|
||||||
|
lsblk_process = subprocess.Popen(
|
||||||
|
lsblk_command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
shell=True)
|
||||||
|
lsblk_output = lsblk_process.communicate()[0]
|
||||||
|
if lsblk_process.returncode == 0:
|
||||||
|
model_num = lsblk_output.strip()
|
||||||
|
else:
|
||||||
|
# both hdparm and lsblk commands failed, try udev
|
||||||
|
model_num = device.get('ID_MODEL')
|
||||||
|
if not model_num:
|
||||||
|
model_num = constants.DEVICE_MODEL_UNKNOWN
|
||||||
|
except Exception as e:
|
||||||
|
self.handle_exception("Could not retrieve disk model "
|
||||||
|
"for disk %s. Exception: %s" %
|
||||||
|
(device.get('DEVNAME'), e))
|
||||||
|
try:
|
||||||
|
if 'ID_SCSI_SERIAL' in device:
|
||||||
|
serial_id = device['ID_SCSI_SERIAL']
|
||||||
|
else:
|
||||||
|
serial_id = device['ID_SERIAL_SHORT']
|
||||||
|
except Exception as e:
|
||||||
|
self.handle_exception("Could not retrieve disk "
|
||||||
|
"serial ID - %s " % e)
|
||||||
|
|
||||||
|
capabilities = dict()
|
||||||
|
if model_num:
|
||||||
|
capabilities.update({'model_num': model_num})
|
||||||
|
|
||||||
|
if self.get_rootfs_node() == device.device_node:
|
||||||
|
capabilities.update({'stor_function': 'rootfs'})
|
||||||
|
|
||||||
|
rotational = self.is_rotational(device)
|
||||||
|
device_type = device.device_type
|
||||||
|
|
||||||
|
rotation_rate = constants.DEVICE_TYPE_UNDETERMINED
|
||||||
|
if rotational is '1':
|
||||||
|
device_type = constants.DEVICE_TYPE_HDD
|
||||||
|
if 'ID_ATA_ROTATION_RATE_RPM' in device:
|
||||||
|
rotation_rate = device['ID_ATA_ROTATION_RATE_RPM']
|
||||||
|
elif rotational is '0':
|
||||||
|
if constants.DEVICE_NAME_NVME in device.device_node:
|
||||||
|
device_type = constants.DEVICE_TYPE_NVME
|
||||||
|
else:
|
||||||
|
device_type = constants.DEVICE_TYPE_SSD
|
||||||
|
rotation_rate = constants.DEVICE_TYPE_NA
|
||||||
|
|
||||||
|
# TODO(sc) else: what are other possible stor_function value?
|
||||||
|
# or do we just use pair { 'is_rootfs': True } instead?
|
||||||
|
# Obtain device ID and WWN.
|
||||||
|
device_id, device_wwn = self.get_device_id_wwn(device)
|
||||||
|
|
||||||
|
attr = {
|
||||||
|
'device_node': device.device_node,
|
||||||
|
'device_num': device.device_number,
|
||||||
|
'device_type': device_type,
|
||||||
|
'device_path': device_path,
|
||||||
|
'device_id': device_id,
|
||||||
|
'device_wwn': device_wwn,
|
||||||
|
'size_mib': size_mib,
|
||||||
|
'available_mib': available_mib,
|
||||||
|
'serial_id': serial_id,
|
||||||
|
'capabilities': capabilities,
|
||||||
|
'rpm': rotation_rate,
|
||||||
|
}
|
||||||
|
|
||||||
|
idisk.append(attr)
|
||||||
|
|
||||||
|
LOG.debug("idisk= %s" % idisk)
|
||||||
|
|
||||||
|
return idisk
|
|
@ -0,0 +1,23 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils._i18n import _
|
||||||
|
|
||||||
|
INVENTORY_LLDP_OPTS = [
|
||||||
|
cfg.ListOpt('drivers',
|
||||||
|
default=['lldpd'],
|
||||||
|
help=_("An ordered list of inventory LLDP driver "
|
||||||
|
"entrypoints to be loaded from the "
|
||||||
|
"inventory.agent namespace.")),
|
||||||
|
]
|
||||||
|
|
||||||
|
cfg.CONF.register_opts(INVENTORY_LLDP_OPTS, group="lldp")
|
|
@ -0,0 +1,47 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class InventoryLldpDriverBase(object):
|
||||||
|
"""Inventory LLDP Driver Base Class."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def lldp_has_neighbour(self, name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def lldp_update(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def lldp_agents_list(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def lldp_neighbours_list(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def lldp_agents_clear(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def lldp_neighbours_clear(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def lldp_update_systemname(self, systemname):
|
||||||
|
pass
|
|
@ -0,0 +1,321 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
import simplejson as json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from inventory.agent.lldp.drivers import base
|
||||||
|
from inventory.agent.lldp import plugin
|
||||||
|
from inventory.common import k_lldp
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryLldpdAgentDriver(base.InventoryLldpDriverBase):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.client = ""
|
||||||
|
self.agents = []
|
||||||
|
self.neighbours = []
|
||||||
|
self.current_neighbours = []
|
||||||
|
self.previous_neighbours = []
|
||||||
|
self.current_agents = []
|
||||||
|
self.previous_agents = []
|
||||||
|
self.agent_audit_count = 0
|
||||||
|
self.neighbour_audit_count = 0
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.__init__()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _lldpd_get_agent_status():
|
||||||
|
json_obj = json
|
||||||
|
p = subprocess.Popen(["lldpcli", "-f", "json", "show",
|
||||||
|
"configuration"],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
data = json_obj.loads(p.communicate()[0])
|
||||||
|
|
||||||
|
configuration = data['configuration'][0]
|
||||||
|
config = configuration['config'][0]
|
||||||
|
rx_only = config['rx-only'][0]
|
||||||
|
|
||||||
|
if rx_only.get("value") == "no":
|
||||||
|
return "rx=enabled,tx=enabled"
|
||||||
|
else:
|
||||||
|
return "rx=enabled,tx=disabled"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _lldpd_get_attrs(iface):
|
||||||
|
name_or_uuid = None
|
||||||
|
chassis_id = None
|
||||||
|
system_name = None
|
||||||
|
system_desc = None
|
||||||
|
capability = None
|
||||||
|
management_address = None
|
||||||
|
port_desc = None
|
||||||
|
dot1_lag = None
|
||||||
|
dot1_port_vid = None
|
||||||
|
dot1_vid_digest = None
|
||||||
|
dot1_mgmt_vid = None
|
||||||
|
dot1_vlan_names = None
|
||||||
|
dot1_proto_vids = None
|
||||||
|
dot1_proto_ids = None
|
||||||
|
dot3_mac_status = None
|
||||||
|
dot3_max_frame = None
|
||||||
|
dot3_power_mdi = None
|
||||||
|
ttl = None
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
# Note: dot1_vid_digest, dot1_mgmt_vid are not currently supported
|
||||||
|
# by the lldpd daemon
|
||||||
|
|
||||||
|
name_or_uuid = iface.get("name")
|
||||||
|
chassis = iface.get("chassis")[0]
|
||||||
|
port = iface.get("port")[0]
|
||||||
|
|
||||||
|
if not chassis.get('id'):
|
||||||
|
return attrs
|
||||||
|
chassis_id = chassis['id'][0].get("value")
|
||||||
|
|
||||||
|
if not port.get('id'):
|
||||||
|
return attrs
|
||||||
|
port_id = port["id"][0].get("value")
|
||||||
|
|
||||||
|
if not port.get('ttl'):
|
||||||
|
return attrs
|
||||||
|
ttl = port['ttl'][0].get("value")
|
||||||
|
|
||||||
|
if chassis.get("name"):
|
||||||
|
system_name = chassis['name'][0].get("value")
|
||||||
|
|
||||||
|
if chassis.get("descr"):
|
||||||
|
system_desc = chassis['descr'][0].get("value")
|
||||||
|
|
||||||
|
if chassis.get("capability"):
|
||||||
|
capability = ""
|
||||||
|
for cap in chassis["capability"]:
|
||||||
|
if cap.get("enabled"):
|
||||||
|
if capability:
|
||||||
|
capability += ", "
|
||||||
|
capability += cap.get("type").lower()
|
||||||
|
|
||||||
|
if chassis.get("mgmt-ip"):
|
||||||
|
management_address = ""
|
||||||
|
for addr in chassis["mgmt-ip"]:
|
||||||
|
if management_address:
|
||||||
|
management_address += ", "
|
||||||
|
management_address += addr.get("value").lower()
|
||||||
|
|
||||||
|
if port.get("descr"):
|
||||||
|
port_desc = port["descr"][0].get("value")
|
||||||
|
|
||||||
|
if port.get("link-aggregation"):
|
||||||
|
dot1_lag_supported = port["link-aggregation"][0].get("supported")
|
||||||
|
dot1_lag_enabled = port["link-aggregation"][0].get("enabled")
|
||||||
|
dot1_lag = "capable="
|
||||||
|
if dot1_lag_supported:
|
||||||
|
dot1_lag += "y,"
|
||||||
|
else:
|
||||||
|
dot1_lag += "n,"
|
||||||
|
dot1_lag += "enabled="
|
||||||
|
if dot1_lag_enabled:
|
||||||
|
dot1_lag += "y"
|
||||||
|
else:
|
||||||
|
dot1_lag += "n"
|
||||||
|
|
||||||
|
if port.get("auto-negotiation"):
|
||||||
|
port_auto_neg_support = port["auto-negotiation"][0].get(
|
||||||
|
"supported")
|
||||||
|
port_auto_neg_enabled = port["auto-negotiation"][0].get("enabled")
|
||||||
|
dot3_mac_status = "auto-negotiation-capable="
|
||||||
|
if port_auto_neg_support:
|
||||||
|
dot3_mac_status += "y,"
|
||||||
|
else:
|
||||||
|
dot3_mac_status += "n,"
|
||||||
|
dot3_mac_status += "auto-negotiation-enabled="
|
||||||
|
if port_auto_neg_enabled:
|
||||||
|
dot3_mac_status += "y,"
|
||||||
|
else:
|
||||||
|
dot3_mac_status += "n,"
|
||||||
|
advertised = ""
|
||||||
|
if port.get("auto-negotiation")[0].get("advertised"):
|
||||||
|
for adv in port["auto-negotiation"][0].get("advertised"):
|
||||||
|
if advertised:
|
||||||
|
advertised += ", "
|
||||||
|
type = adv.get("type").lower()
|
||||||
|
if adv.get("hd") and not adv.get("fd"):
|
||||||
|
type += "hd"
|
||||||
|
elif adv.get("fd"):
|
||||||
|
type += "fd"
|
||||||
|
advertised += type
|
||||||
|
dot3_mac_status += advertised
|
||||||
|
|
||||||
|
if port.get("mfs"):
|
||||||
|
dot3_max_frame = port["mfs"][0].get("value")
|
||||||
|
|
||||||
|
if port.get("power"):
|
||||||
|
power_mdi_support = port["power"][0].get("supported")
|
||||||
|
power_mdi_enabled = port["power"][0].get("enabled")
|
||||||
|
power_mdi_devicetype = port["power"][0].get("device-type")[0].get(
|
||||||
|
"value")
|
||||||
|
power_mdi_pairs = port["power"][0].get("pairs")[0].get("value")
|
||||||
|
power_mdi_class = port["power"][0].get("class")[0].get("value")
|
||||||
|
dot3_power_mdi = "power-mdi-supported="
|
||||||
|
if power_mdi_support:
|
||||||
|
dot3_power_mdi += "y,"
|
||||||
|
else:
|
||||||
|
dot3_power_mdi += "n,"
|
||||||
|
dot3_power_mdi += "power-mdi-enabled="
|
||||||
|
if power_mdi_enabled:
|
||||||
|
dot3_power_mdi += "y,"
|
||||||
|
else:
|
||||||
|
dot3_power_mdi += "n,"
|
||||||
|
if power_mdi_support and power_mdi_enabled:
|
||||||
|
dot3_power_mdi += "device-type=" + power_mdi_devicetype
|
||||||
|
dot3_power_mdi += ",pairs=" + power_mdi_pairs
|
||||||
|
dot3_power_mdi += ",class=" + power_mdi_class
|
||||||
|
|
||||||
|
vlans = None
|
||||||
|
if iface.get("vlan"):
|
||||||
|
vlans = iface.get("vlan")
|
||||||
|
|
||||||
|
if vlans:
|
||||||
|
dot1_vlan_names = ""
|
||||||
|
for vlan in vlans:
|
||||||
|
if vlan.get("pvid"):
|
||||||
|
dot1_port_vid = vlan.get("vlan-id")
|
||||||
|
continue
|
||||||
|
if dot1_vlan_names:
|
||||||
|
dot1_vlan_names += ", "
|
||||||
|
dot1_vlan_names += vlan.get("value")
|
||||||
|
|
||||||
|
ppvids = None
|
||||||
|
if iface.get("ppvids"):
|
||||||
|
ppvids = iface.get("ppvid")
|
||||||
|
|
||||||
|
if ppvids:
|
||||||
|
dot1_proto_vids = ""
|
||||||
|
for ppvid in ppvids:
|
||||||
|
if dot1_proto_vids:
|
||||||
|
dot1_proto_vids += ", "
|
||||||
|
dot1_proto_vids += ppvid.get("value")
|
||||||
|
|
||||||
|
pids = None
|
||||||
|
if iface.get("pi"):
|
||||||
|
pids = iface.get('pi')
|
||||||
|
dot1_proto_ids = ""
|
||||||
|
for id in pids:
|
||||||
|
if dot1_proto_ids:
|
||||||
|
dot1_proto_ids += ", "
|
||||||
|
dot1_proto_ids += id.get("value")
|
||||||
|
|
||||||
|
msap = chassis_id + "," + port_id
|
||||||
|
|
||||||
|
attrs = {"name_or_uuid": name_or_uuid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_CHASSIS_ID: chassis_id,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_ID: port_id,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_TTL: ttl,
|
||||||
|
"msap": msap,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_NAME: system_name,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_DESC: system_desc,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_CAP: capability,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_MGMT_ADDR: management_address,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_DESC: port_desc,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_LAG: dot1_lag,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PORT_VID: dot1_port_vid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VID_DIGEST: dot1_vid_digest,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_MGMT_VID: dot1_mgmt_vid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VLAN_NAMES: dot1_vlan_names,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_VIDS: dot1_proto_vids,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_IDS: dot1_proto_ids,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAC_STATUS: dot3_mac_status,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAX_FRAME: dot3_max_frame,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_POWER_MDI: dot3_power_mdi}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def lldp_has_neighbour(self, name):
|
||||||
|
p = subprocess.check_output(["lldpcli", "-f", "keyvalue", "show",
|
||||||
|
"neighbors", "summary", "ports", name])
|
||||||
|
return len(p) > 0
|
||||||
|
|
||||||
|
def lldp_update(self):
|
||||||
|
subprocess.call(['lldpcli', 'update'])
|
||||||
|
|
||||||
|
def lldp_agents_list(self):
|
||||||
|
json_obj = json
|
||||||
|
lldp_agents = []
|
||||||
|
|
||||||
|
p = subprocess.Popen(["lldpcli", "-f", "json", "show", "interface",
|
||||||
|
"detail"], stdout=subprocess.PIPE)
|
||||||
|
data = json_obj.loads(p.communicate()[0])
|
||||||
|
|
||||||
|
lldp = data['lldp'][0]
|
||||||
|
|
||||||
|
if not lldp.get('interface'):
|
||||||
|
return lldp_agents
|
||||||
|
|
||||||
|
for iface in lldp['interface']:
|
||||||
|
agent_attrs = self._lldpd_get_attrs(iface)
|
||||||
|
status = self._lldpd_get_agent_status()
|
||||||
|
agent_attrs.update({"status": status})
|
||||||
|
agent = plugin.Agent(**agent_attrs)
|
||||||
|
lldp_agents.append(agent)
|
||||||
|
|
||||||
|
return lldp_agents
|
||||||
|
|
||||||
|
def lldp_agents_clear(self):
|
||||||
|
self.current_agents = []
|
||||||
|
self.previous_agents = []
|
||||||
|
|
||||||
|
def lldp_neighbours_list(self):
|
||||||
|
json_obj = json
|
||||||
|
lldp_neighbours = []
|
||||||
|
p = subprocess.Popen(["lldpcli", "-f", "json", "show", "neighbor",
|
||||||
|
"detail"], stdout=subprocess.PIPE)
|
||||||
|
data = json_obj.loads(p.communicate()[0])
|
||||||
|
|
||||||
|
lldp = data['lldp'][0]
|
||||||
|
|
||||||
|
if not lldp.get('interface'):
|
||||||
|
return lldp_neighbours
|
||||||
|
|
||||||
|
for iface in lldp['interface']:
|
||||||
|
neighbour_attrs = self._lldpd_get_attrs(iface)
|
||||||
|
neighbour = plugin.Neighbour(**neighbour_attrs)
|
||||||
|
lldp_neighbours.append(neighbour)
|
||||||
|
|
||||||
|
return lldp_neighbours
|
||||||
|
|
||||||
|
def lldp_neighbours_clear(self):
|
||||||
|
self.current_neighbours = []
|
||||||
|
self.previous_neighbours = []
|
||||||
|
|
||||||
|
def lldp_update_systemname(self, systemname):
|
||||||
|
p = subprocess.Popen(["lldpcli", "-f", "json", "show", "chassis"],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
data = json.loads(p.communicate()[0])
|
||||||
|
|
||||||
|
local_chassis = data['local-chassis'][0]
|
||||||
|
chassis = local_chassis['chassis'][0]
|
||||||
|
name = chassis.get('name', None)
|
||||||
|
if name is None or not name[0].get("value"):
|
||||||
|
return
|
||||||
|
name = name[0]
|
||||||
|
|
||||||
|
hostname = name.get("value").partition(':')[0]
|
||||||
|
|
||||||
|
newname = hostname + ":" + systemname
|
||||||
|
|
||||||
|
p = subprocess.Popen(["lldpcli", "configure", "system", "hostname",
|
||||||
|
newname], stdout=subprocess.PIPE)
|
|
@ -0,0 +1,167 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
import simplejson as json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from inventory.agent.lldp.drivers.lldpd import driver as lldpd_driver
|
||||||
|
from inventory.common import k_lldp
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryOVSAgentDriver(lldpd_driver.InventoryLldpdAgentDriver):
|
||||||
|
|
||||||
|
def run_cmd(self, cmd):
|
||||||
|
p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
p.wait()
|
||||||
|
output, error = p.communicate()
|
||||||
|
if p.returncode != 0:
|
||||||
|
LOG.error("Failed to run command %s: error: %s", cmd, error)
|
||||||
|
return None
|
||||||
|
return output
|
||||||
|
|
||||||
|
def lldp_ovs_get_interface_port_map(self):
|
||||||
|
interface_port_map = {}
|
||||||
|
|
||||||
|
cmd = "ovs-vsctl --timeout 10 --format json "\
|
||||||
|
"--columns name,_uuid,interfaces list Port"
|
||||||
|
|
||||||
|
output = self.run_cmd(cmd)
|
||||||
|
if not output:
|
||||||
|
return
|
||||||
|
|
||||||
|
ports = json.loads(output)
|
||||||
|
ports = ports['data']
|
||||||
|
|
||||||
|
for port in ports:
|
||||||
|
port_uuid = port[1][1]
|
||||||
|
interfaces = port[2][1]
|
||||||
|
|
||||||
|
if isinstance(interfaces, list):
|
||||||
|
for interface in interfaces:
|
||||||
|
interface_uuid = interface[1]
|
||||||
|
interface_port_map[interface_uuid] = port_uuid
|
||||||
|
else:
|
||||||
|
interface_uuid = interfaces
|
||||||
|
interface_port_map[interface_uuid] = port_uuid
|
||||||
|
|
||||||
|
return interface_port_map
|
||||||
|
|
||||||
|
def lldp_ovs_get_port_bridge_map(self):
|
||||||
|
port_bridge_map = {}
|
||||||
|
|
||||||
|
cmd = "ovs-vsctl --timeout 10 --format json "\
|
||||||
|
"--columns name,ports list Bridge"
|
||||||
|
output = self.run_cmd(cmd)
|
||||||
|
if not output:
|
||||||
|
return
|
||||||
|
|
||||||
|
bridges = json.loads(output)
|
||||||
|
bridges = bridges['data']
|
||||||
|
|
||||||
|
for bridge in bridges:
|
||||||
|
bridge_name = bridge[0]
|
||||||
|
port_set = bridge[1][1]
|
||||||
|
for port in port_set:
|
||||||
|
value = port[1]
|
||||||
|
port_bridge_map[value] = bridge_name
|
||||||
|
|
||||||
|
return port_bridge_map
|
||||||
|
|
||||||
|
def lldp_ovs_lldp_flow_exists(self, brname, in_port):
|
||||||
|
|
||||||
|
cmd = "ovs-ofctl dump-flows {} in_port={},dl_dst={},dl_type={}".format(
|
||||||
|
brname, in_port, k_lldp.LLDP_MULTICAST_ADDRESS,
|
||||||
|
k_lldp.LLDP_ETHER_TYPE)
|
||||||
|
output = self.run_cmd(cmd)
|
||||||
|
if not output:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (output.count("\n") > 1)
|
||||||
|
|
||||||
|
def lldp_ovs_add_flows(self, brname, in_port, out_port):
|
||||||
|
|
||||||
|
cmd = ("ovs-ofctl add-flow {} in_port={},dl_dst={},dl_type={},"
|
||||||
|
"actions=output:{}".format(
|
||||||
|
brname, in_port, k_lldp.LLDP_MULTICAST_ADDRESS,
|
||||||
|
k_lldp.LLDP_ETHER_TYPE, out_port))
|
||||||
|
output = self.run_cmd(cmd)
|
||||||
|
if not output:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = ("ovs-ofctl add-flow {} in_port={},dl_dst={},dl_type={},"
|
||||||
|
"actions=output:{}".format(
|
||||||
|
brname, out_port, k_lldp.LLDP_MULTICAST_ADDRESS,
|
||||||
|
k_lldp.LLDP_ETHER_TYPE, in_port))
|
||||||
|
output = self.run_cmd(cmd)
|
||||||
|
if not output:
|
||||||
|
return
|
||||||
|
|
||||||
|
def lldp_ovs_update_flows(self):
|
||||||
|
|
||||||
|
port_bridge_map = self.lldp_ovs_get_port_bridge_map()
|
||||||
|
if not port_bridge_map:
|
||||||
|
return
|
||||||
|
|
||||||
|
interface_port_map = self.lldp_ovs_get_interface_port_map()
|
||||||
|
if not interface_port_map:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = "ovs-vsctl --timeout 10 --format json "\
|
||||||
|
"--columns name,_uuid,type,other_config list Interface"
|
||||||
|
|
||||||
|
output = self.run_cmd(cmd)
|
||||||
|
if not output:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = json.loads(output)
|
||||||
|
data = data['data']
|
||||||
|
|
||||||
|
for interface in data:
|
||||||
|
name = interface[0]
|
||||||
|
uuid = interface[1][1]
|
||||||
|
type = interface[2]
|
||||||
|
other_config = interface[3]
|
||||||
|
|
||||||
|
if type != 'internal':
|
||||||
|
continue
|
||||||
|
|
||||||
|
config_map = other_config[1]
|
||||||
|
for config in config_map:
|
||||||
|
key = config[0]
|
||||||
|
value = config[1]
|
||||||
|
if key != 'lldp_phy_peer':
|
||||||
|
continue
|
||||||
|
|
||||||
|
phy_peer = value
|
||||||
|
brname = port_bridge_map[interface_port_map[uuid]]
|
||||||
|
if not self.lldp_ovs_lldp_flow_exists(brname, name):
|
||||||
|
LOG.info("Adding missing LLDP flow from %s to %s",
|
||||||
|
name, phy_peer)
|
||||||
|
self.lldp_ovs_add_flows(brname, name, phy_peer)
|
||||||
|
|
||||||
|
if not self.lldp_ovs_lldp_flow_exists(brname, value):
|
||||||
|
LOG.info("Adding missing LLDP flow from %s to %s",
|
||||||
|
phy_peer, name)
|
||||||
|
self.lldp_ovs_add_flows(brname, phy_peer, name)
|
||||||
|
|
||||||
|
def lldp_agents_list(self):
|
||||||
|
self.lldp_ovs_update_flows()
|
||||||
|
return lldpd_driver.InventoryLldpdAgentDriver.lldp_agents_list(self)
|
||||||
|
|
||||||
|
def lldp_neighbours_list(self):
|
||||||
|
self.lldp_ovs_update_flows()
|
||||||
|
return lldpd_driver.InventoryLldpdAgentDriver.lldp_neighbours_list(
|
||||||
|
self)
|
|
@ -0,0 +1,176 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
from inventory.common import exception
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
from stevedore.named import NamedExtensionManager
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
cfg.CONF.import_opt('drivers',
|
||||||
|
'inventory.agent.lldp.config',
|
||||||
|
group='lldp')
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryLldpDriverManager(NamedExtensionManager):
|
||||||
|
"""Implementation of Inventory LLDP drivers."""
|
||||||
|
|
||||||
|
def __init__(self, namespace='inventory.agent.lldp.drivers'):
|
||||||
|
|
||||||
|
# Registered inventory lldp agent drivers, keyed by name.
|
||||||
|
self.drivers = {}
|
||||||
|
|
||||||
|
# Ordered list of inventory lldp agent drivers, defining
|
||||||
|
# the order in which the drivers are called.
|
||||||
|
self.ordered_drivers = []
|
||||||
|
|
||||||
|
names = cfg.CONF.lldp.drivers
|
||||||
|
LOG.info("Configured inventory LLDP agent drivers: %s", names)
|
||||||
|
|
||||||
|
super(InventoryLldpDriverManager, self).__init__(
|
||||||
|
namespace,
|
||||||
|
names,
|
||||||
|
invoke_on_load=True,
|
||||||
|
name_order=True)
|
||||||
|
|
||||||
|
LOG.info("Loaded inventory LLDP agent drivers: %s", self.names())
|
||||||
|
self._register_drivers()
|
||||||
|
|
||||||
|
def _register_drivers(self):
|
||||||
|
"""Register all inventory LLDP agent drivers.
|
||||||
|
|
||||||
|
This method should only be called once in the
|
||||||
|
InventoryLldpDriverManager constructor.
|
||||||
|
"""
|
||||||
|
for ext in self:
|
||||||
|
self.drivers[ext.name] = ext
|
||||||
|
self.ordered_drivers.append(ext)
|
||||||
|
LOG.info("Registered inventory LLDP agent drivers: %s",
|
||||||
|
[driver.name for driver in self.ordered_drivers])
|
||||||
|
|
||||||
|
def _call_drivers_and_return_array(self, method_name, attr=None,
|
||||||
|
raise_orig_exc=False):
|
||||||
|
"""Helper method for calling a method across all drivers.
|
||||||
|
|
||||||
|
:param method_name: name of the method to call
|
||||||
|
:param attr: an optional attribute to provide to the drivers
|
||||||
|
:param raise_orig_exc: whether or not to raise the original
|
||||||
|
driver exception, or use a general one
|
||||||
|
"""
|
||||||
|
ret = []
|
||||||
|
for driver in self.ordered_drivers:
|
||||||
|
try:
|
||||||
|
method = getattr(driver.obj, method_name)
|
||||||
|
if attr:
|
||||||
|
ret = ret + method(attr)
|
||||||
|
else:
|
||||||
|
ret = ret + method()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
LOG.error(
|
||||||
|
"Inventory LLDP agent driver '%(name)s' "
|
||||||
|
"failed in %(method)s",
|
||||||
|
{'name': driver.name, 'method': method_name}
|
||||||
|
)
|
||||||
|
if raise_orig_exc:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise exception.LLDPDriverError(
|
||||||
|
method=method_name
|
||||||
|
)
|
||||||
|
return list(set(ret))
|
||||||
|
|
||||||
|
def _call_drivers(self, method_name, attr=None, raise_orig_exc=False):
|
||||||
|
"""Helper method for calling a method across all drivers.
|
||||||
|
|
||||||
|
:param method_name: name of the method to call
|
||||||
|
:param attr: an optional attribute to provide to the drivers
|
||||||
|
:param raise_orig_exc: whether or not to raise the original
|
||||||
|
driver exception, or use a general one
|
||||||
|
"""
|
||||||
|
for driver in self.ordered_drivers:
|
||||||
|
try:
|
||||||
|
method = getattr(driver.obj, method_name)
|
||||||
|
if attr:
|
||||||
|
method(attr)
|
||||||
|
else:
|
||||||
|
method()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
LOG.error(
|
||||||
|
"Inventory LLDP agent driver '%(name)s' "
|
||||||
|
"failed in %(method)s",
|
||||||
|
{'name': driver.name, 'method': method_name}
|
||||||
|
)
|
||||||
|
if raise_orig_exc:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise exception.LLDPDriverError(
|
||||||
|
method=method_name
|
||||||
|
)
|
||||||
|
|
||||||
|
def lldp_has_neighbour(self, name):
|
||||||
|
try:
|
||||||
|
return self._call_drivers("lldp_has_neighbour",
|
||||||
|
attr=name,
|
||||||
|
raise_orig_exc=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def lldp_update(self):
|
||||||
|
try:
|
||||||
|
return self._call_drivers("lldp_update",
|
||||||
|
raise_orig_exc=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def lldp_agents_list(self):
|
||||||
|
try:
|
||||||
|
return self._call_drivers_and_return_array("lldp_agents_list",
|
||||||
|
raise_orig_exc=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def lldp_neighbours_list(self):
|
||||||
|
try:
|
||||||
|
return self._call_drivers_and_return_array("lldp_neighbours_list",
|
||||||
|
raise_orig_exc=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def lldp_agents_clear(self):
|
||||||
|
try:
|
||||||
|
return self._call_drivers("lldp_agents_clear",
|
||||||
|
raise_orig_exc=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
def lldp_neighbours_clear(self):
|
||||||
|
try:
|
||||||
|
return self._call_drivers("lldp_neighbours_clear",
|
||||||
|
raise_orig_exc=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
def lldp_update_systemname(self, systemname):
|
||||||
|
try:
|
||||||
|
return self._call_drivers("lldp_update_systemname",
|
||||||
|
attr=systemname,
|
||||||
|
raise_orig_exc=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
return
|
|
@ -0,0 +1,246 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
|
from inventory.agent.lldp import manager
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common import k_lldp
|
||||||
|
from inventory.common.utils import compare as cmp
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Key(object):
|
||||||
|
def __init__(self, chassisid, portid, portname):
|
||||||
|
self.chassisid = chassisid
|
||||||
|
self.portid = portid
|
||||||
|
self.portname = portname
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.chassisid, self.portid, self.portname))
|
||||||
|
|
||||||
|
def __cmp__(self, rhs):
|
||||||
|
return (cmp(self.chassisid, rhs.chassisid) or
|
||||||
|
cmp(self.portid, rhs.portid) or
|
||||||
|
cmp(self.portname, rhs.portname))
|
||||||
|
|
||||||
|
def __eq__(self, rhs):
|
||||||
|
return (self.chassisid == rhs.chassisid and
|
||||||
|
self.portid == rhs.portid and
|
||||||
|
self.portname == rhs.portname)
|
||||||
|
|
||||||
|
def __ne__(self, rhs):
|
||||||
|
return (self.chassisid != rhs.chassisid or
|
||||||
|
self.portid != rhs.portid or
|
||||||
|
self.portname != rhs.portname)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s [%s] [%s]" % (self.portname, self.chassisid, self.portid)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Key '%s'>" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(object):
|
||||||
|
'''Class to encapsulate LLDP agent data for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
'''Construct an Agent object with the given values.'''
|
||||||
|
self.key = Key(kwargs.get(k_lldp.LLDP_TLV_TYPE_CHASSIS_ID),
|
||||||
|
kwargs.get(k_lldp.LLDP_TLV_TYPE_PORT_ID),
|
||||||
|
kwargs.get("name_or_uuid"))
|
||||||
|
self.status = kwargs.get('status')
|
||||||
|
self.ttl = kwargs.get(k_lldp.LLDP_TLV_TYPE_TTL)
|
||||||
|
self.system_name = kwargs.get(k_lldp.LLDP_TLV_TYPE_SYSTEM_NAME)
|
||||||
|
self.system_desc = kwargs.get(k_lldp.LLDP_TLV_TYPE_SYSTEM_DESC)
|
||||||
|
self.port_desc = kwargs.get(k_lldp.LLDP_TLV_TYPE_PORT_DESC)
|
||||||
|
self.capabilities = kwargs.get(k_lldp.LLDP_TLV_TYPE_SYSTEM_CAP)
|
||||||
|
self.mgmt_addr = kwargs.get(k_lldp.LLDP_TLV_TYPE_MGMT_ADDR)
|
||||||
|
self.dot1_lag = kwargs.get(k_lldp.LLDP_TLV_TYPE_DOT1_LAG)
|
||||||
|
self.dot1_vlan_names = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VLAN_NAMES)
|
||||||
|
self.dot3_max_frame = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAX_FRAME)
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.key.__hash__()
|
||||||
|
|
||||||
|
def __eq__(self, rhs):
|
||||||
|
return (self.key == rhs.key)
|
||||||
|
|
||||||
|
def __ne__(self, rhs):
|
||||||
|
return (self.key != rhs.key or
|
||||||
|
self.status != rhs.status or
|
||||||
|
self.ttl != rhs.ttl or
|
||||||
|
self.system_name != rhs.system_name or
|
||||||
|
self.system_desc != rhs.system_desc or
|
||||||
|
self.port_desc != rhs.port_desc or
|
||||||
|
self.capabilities != rhs.capabilities or
|
||||||
|
self.mgmt_addr != rhs.mgmt_addr or
|
||||||
|
self.dot1_lag != rhs.dot1_lag or
|
||||||
|
self.dot1_vlan_names != rhs.dot1_vlan_names or
|
||||||
|
self.dot3_max_frame != rhs.dot3_max_frame or
|
||||||
|
self.state != rhs.state)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s: [%s] [%s] [%s], [%s], [%s], [%s], [%s], [%s]" % (
|
||||||
|
self.key, self.status, self.system_name, self.system_desc,
|
||||||
|
self.port_desc, self.capabilities,
|
||||||
|
self.mgmt_addr, self.dot1_lag,
|
||||||
|
self.dot3_max_frame)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Agent '%s'>" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class Neighbour(object):
|
||||||
|
'''Class to encapsulate LLDP neighbour data for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
'''Construct an Neighbour object with the given values.'''
|
||||||
|
self.key = Key(kwargs.get(k_lldp.LLDP_TLV_TYPE_CHASSIS_ID),
|
||||||
|
kwargs.get(k_lldp.LLDP_TLV_TYPE_PORT_ID),
|
||||||
|
kwargs.get("name_or_uuid"))
|
||||||
|
self.msap = kwargs.get('msap')
|
||||||
|
self.ttl = kwargs.get(k_lldp.LLDP_TLV_TYPE_TTL)
|
||||||
|
self.system_name = kwargs.get(k_lldp.LLDP_TLV_TYPE_SYSTEM_NAME)
|
||||||
|
self.system_desc = kwargs.get(k_lldp.LLDP_TLV_TYPE_SYSTEM_DESC)
|
||||||
|
self.port_desc = kwargs.get(k_lldp.LLDP_TLV_TYPE_PORT_DESC)
|
||||||
|
self.capabilities = kwargs.get(k_lldp.LLDP_TLV_TYPE_SYSTEM_CAP)
|
||||||
|
self.mgmt_addr = kwargs.get(k_lldp.LLDP_TLV_TYPE_MGMT_ADDR)
|
||||||
|
self.dot1_port_vid = kwargs.get(k_lldp.LLDP_TLV_TYPE_DOT1_PORT_VID)
|
||||||
|
self.dot1_vid_digest = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VID_DIGEST)
|
||||||
|
self.dot1_mgmt_vid = kwargs.get(k_lldp.LLDP_TLV_TYPE_DOT1_MGMT_VID)
|
||||||
|
self.dot1_vid_digest = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VID_DIGEST)
|
||||||
|
self.dot1_mgmt_vid = kwargs.get(k_lldp.LLDP_TLV_TYPE_DOT1_MGMT_VID)
|
||||||
|
self.dot1_lag = kwargs.get(k_lldp.LLDP_TLV_TYPE_DOT1_LAG)
|
||||||
|
self.dot1_vlan_names = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VLAN_NAMES)
|
||||||
|
self.dot1_proto_vids = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_VIDS)
|
||||||
|
self.dot1_proto_ids = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_IDS)
|
||||||
|
self.dot3_mac_status = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAC_STATUS)
|
||||||
|
self.dot3_max_frame = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAX_FRAME)
|
||||||
|
self.dot3_power_mdi = kwargs.get(
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_POWER_MDI)
|
||||||
|
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.key.__hash__()
|
||||||
|
|
||||||
|
def __eq__(self, rhs):
|
||||||
|
return (self.key == rhs.key)
|
||||||
|
|
||||||
|
def __ne__(self, rhs):
|
||||||
|
return (self.key != rhs.key or
|
||||||
|
self.msap != rhs.msap or
|
||||||
|
self.system_name != rhs.system_name or
|
||||||
|
self.system_desc != rhs.system_desc or
|
||||||
|
self.port_desc != rhs.port_desc or
|
||||||
|
self.capabilities != rhs.capabilities or
|
||||||
|
self.mgmt_addr != rhs.mgmt_addr or
|
||||||
|
self.dot1_port_vid != rhs.dot1_port_vid or
|
||||||
|
self.dot1_vid_digest != rhs.dot1_vid_digest or
|
||||||
|
self.dot1_mgmt_vid != rhs.dot1_mgmt_vid or
|
||||||
|
self.dot1_vid_digest != rhs.dot1_vid_digest or
|
||||||
|
self.dot1_mgmt_vid != rhs.dot1_mgmt_vid or
|
||||||
|
self.dot1_lag != rhs.dot1_lag or
|
||||||
|
self.dot1_vlan_names != rhs.dot1_vlan_names or
|
||||||
|
self.dot1_proto_vids != rhs.dot1_proto_vids or
|
||||||
|
self.dot1_proto_ids != rhs.dot1_proto_ids or
|
||||||
|
self.dot3_mac_status != rhs.dot3_mac_status or
|
||||||
|
self.dot3_max_frame != rhs.dot3_max_frame or
|
||||||
|
self.dot3_power_mdi != rhs.dot3_power_mdi)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s [%s] [%s] [%s], [%s]" % (
|
||||||
|
self.key, self.system_name, self.system_desc,
|
||||||
|
self.port_desc, self.capabilities)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Neighbour '%s'>" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryLldpPlugin(object):
|
||||||
|
|
||||||
|
"""Implementation of the Plugin."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.manager = manager.InventoryLldpDriverManager()
|
||||||
|
|
||||||
|
def lldp_has_neighbour(self, name):
|
||||||
|
try:
|
||||||
|
return self.manager.lldp_has_neighbour(name)
|
||||||
|
except exception.LLDPDriverError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("LLDP has neighbour failed")
|
||||||
|
|
||||||
|
def lldp_update(self):
|
||||||
|
try:
|
||||||
|
self.manager.lldp_update()
|
||||||
|
except exception.LLDPDriverError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("LLDP update failed")
|
||||||
|
|
||||||
|
def lldp_agents_list(self):
|
||||||
|
try:
|
||||||
|
agents = self.manager.lldp_agents_list()
|
||||||
|
except exception.LLDPDriverError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("LLDP agents list failed")
|
||||||
|
|
||||||
|
return agents
|
||||||
|
|
||||||
|
def lldp_agents_clear(self):
|
||||||
|
try:
|
||||||
|
self.manager.lldp_agents_clear()
|
||||||
|
except exception.LLDPDriverError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("LLDP agents clear failed")
|
||||||
|
|
||||||
|
def lldp_neighbours_list(self):
|
||||||
|
try:
|
||||||
|
neighbours = self.manager.lldp_neighbours_list()
|
||||||
|
except exception.LLDPDriverError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("LLDP neighbours list failed")
|
||||||
|
|
||||||
|
return neighbours
|
||||||
|
|
||||||
|
def lldp_neighbours_clear(self):
|
||||||
|
try:
|
||||||
|
self.manager.lldp_neighbours_clear()
|
||||||
|
except exception.LLDPDriverError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("LLDP neighbours clear failed")
|
||||||
|
|
||||||
|
def lldp_update_systemname(self, systemname):
|
||||||
|
try:
|
||||||
|
self.manager.lldp_update_systemname(systemname)
|
||||||
|
except exception.LLDPDriverError as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("LLDP update systemname failed")
|
|
@ -0,0 +1,973 @@
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
""" Perform activity related to local inventory.
|
||||||
|
|
||||||
|
A single instance of :py:class:`inventory.agent.manager.AgentManager` is
|
||||||
|
created within the *inventory-agent* process, and is responsible for
|
||||||
|
performing all actions for this host managed by inventory .
|
||||||
|
|
||||||
|
On start, collect and post inventory.
|
||||||
|
|
||||||
|
Commands (from conductors) are received via RPC calls.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
import oslo_messaging as messaging
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
from futurist import periodics
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
# from inventory.agent import partition
|
||||||
|
from inventory.agent import base_manager
|
||||||
|
from inventory.agent.lldp import plugin as lldp_plugin
|
||||||
|
from inventory.agent import node
|
||||||
|
from inventory.agent import pci
|
||||||
|
from inventory.common import constants
|
||||||
|
from inventory.common import context as mycontext
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import k_host
|
||||||
|
from inventory.common import k_lldp
|
||||||
|
from inventory.common import utils
|
||||||
|
from inventory.conductor import rpcapi as conductor_rpcapi
|
||||||
|
import tsconfig.tsconfig as tsc
|
||||||
|
|
||||||
|
MANAGER_TOPIC = 'inventory.agent_manager'
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
agent_opts = [
|
||||||
|
cfg.StrOpt('api_url',
|
||||||
|
default=None,
|
||||||
|
help=('Url of Inventory API service. If not set Inventory can '
|
||||||
|
'get current value from Keystone service catalog.')),
|
||||||
|
cfg.IntOpt('audit_interval',
|
||||||
|
default=60,
|
||||||
|
help='Maximum time since the last check-in of a agent'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(agent_opts, 'agent')
|
||||||
|
|
||||||
|
MAXSLEEP = 300 # 5 minutes
|
||||||
|
|
||||||
|
INVENTORY_READY_FLAG = os.path.join(tsc.VOLATILE_PATH, ".inventory_ready")
|
||||||
|
|
||||||
|
|
||||||
|
FIRST_BOOT_FLAG = os.path.join(
|
||||||
|
tsc.PLATFORM_CONF_PATH, ".first_boot")
|
||||||
|
|
||||||
|
|
||||||
|
class AgentManager(base_manager.BaseAgentManager):
|
||||||
|
"""Inventory Agent service main class."""
|
||||||
|
|
||||||
|
# Must be in sync with rpcapi.AgentAPI's
|
||||||
|
RPC_API_VERSION = '1.0'
|
||||||
|
|
||||||
|
target = messaging.Target(version=RPC_API_VERSION)
|
||||||
|
|
||||||
|
def __init__(self, host, topic):
|
||||||
|
super(AgentManager, self).__init__(host, topic)
|
||||||
|
|
||||||
|
self._report_to_conductor = False
|
||||||
|
self._report_to_conductor_iplatform_avail_flag = False
|
||||||
|
self._ipci_operator = pci.PCIOperator()
|
||||||
|
self._inode_operator = node.NodeOperator()
|
||||||
|
self._lldp_operator = lldp_plugin.InventoryLldpPlugin()
|
||||||
|
self._ihost_personality = None
|
||||||
|
self._ihost_uuid = ""
|
||||||
|
self._agent_throttle = 0
|
||||||
|
self._subfunctions = None
|
||||||
|
self._subfunctions_configured = False
|
||||||
|
self._notify_subfunctions_alarm_clear = False
|
||||||
|
self._notify_subfunctions_alarm_raise = False
|
||||||
|
self._first_grub_update = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_to_conductor_required(self):
|
||||||
|
return self._report_to_conductor
|
||||||
|
|
||||||
|
@report_to_conductor_required.setter
|
||||||
|
def report_to_conductor_required(self, val):
|
||||||
|
if not isinstance(val, bool):
|
||||||
|
raise ValueError("report_to_conductor_required not bool %s" %
|
||||||
|
val)
|
||||||
|
self._report_to_conductor = val
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
# Do not collect inventory and report to conductor at startup in
|
||||||
|
# order to eliminate two inventory reports
|
||||||
|
# (one from here and one from audit) being sent to the conductor
|
||||||
|
|
||||||
|
super(AgentManager, self).start()
|
||||||
|
|
||||||
|
if os.path.isfile('/etc/inventory/inventory.conf'):
|
||||||
|
LOG.info("inventory-agent started, "
|
||||||
|
"inventory to be reported by audit")
|
||||||
|
else:
|
||||||
|
LOG.info("No config file for inventory-agent found.")
|
||||||
|
|
||||||
|
if tsc.system_mode == constants.SYSTEM_MODE_SIMPLEX:
|
||||||
|
utils.touch(INVENTORY_READY_FLAG)
|
||||||
|
|
||||||
|
def init_host(self, admin_context=None):
|
||||||
|
super(AgentManager, self).init_host(admin_context)
|
||||||
|
if os.path.isfile('/etc/inventory/inventory.conf'):
|
||||||
|
LOG.info(_("inventory-agent started, "
|
||||||
|
"system config to be reported by audit"))
|
||||||
|
else:
|
||||||
|
LOG.info(_("No config file for inventory-agent found."))
|
||||||
|
|
||||||
|
if tsc.system_mode == constants.SYSTEM_MODE_SIMPLEX:
|
||||||
|
utils.touch(INVENTORY_READY_FLAG)
|
||||||
|
|
||||||
|
def del_host(self, deregister=True):
|
||||||
|
return
|
||||||
|
|
||||||
|
def periodic_tasks(self, context, raise_on_error=False):
|
||||||
|
"""Periodic tasks are run at pre-specified intervals. """
|
||||||
|
return self.run_periodic_tasks(context,
|
||||||
|
raise_on_error=raise_on_error)
|
||||||
|
|
||||||
|
def _report_to_conductor_iplatform_avail(self):
|
||||||
|
utils.touch(INVENTORY_READY_FLAG)
|
||||||
|
time.sleep(1) # give time for conductor to process
|
||||||
|
self._report_to_conductor_iplatform_avail_flag = True
|
||||||
|
|
||||||
|
def _update_ttys_dcd_status(self, context, host_id):
|
||||||
|
# Retrieve the serial line carrier detect flag
|
||||||
|
ttys_dcd = None
|
||||||
|
rpcapi = conductor_rpcapi.ConductorAPI(
|
||||||
|
topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
try:
|
||||||
|
ttys_dcd = rpcapi.get_host_ttys_dcd(context, host_id)
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.exception("Inventory Agent exception getting host ttys_dcd.")
|
||||||
|
pass
|
||||||
|
if ttys_dcd is not None:
|
||||||
|
self._config_ttys_login(ttys_dcd)
|
||||||
|
else:
|
||||||
|
LOG.debug("ttys_dcd is not configured")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_active_device():
|
||||||
|
# the list of currently configured console devices,
|
||||||
|
# like 'tty1 ttyS0' or just 'ttyS0'
|
||||||
|
# The last entry in the file is the active device connected
|
||||||
|
# to /dev/console.
|
||||||
|
active_device = 'ttyS0'
|
||||||
|
try:
|
||||||
|
cmd = 'cat /sys/class/tty/console/active | grep ttyS'
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
output = proc.stdout.read().strip()
|
||||||
|
proc.communicate()[0]
|
||||||
|
if proc.returncode != 0:
|
||||||
|
LOG.info("Cannot find the current configured serial device, "
|
||||||
|
"return default %s" % active_device)
|
||||||
|
return active_device
|
||||||
|
# if more than one devices are found, take the last entry
|
||||||
|
if ' ' in output:
|
||||||
|
devs = output.split(' ')
|
||||||
|
active_device = devs[len(devs) - 1]
|
||||||
|
else:
|
||||||
|
active_device = output
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
LOG.error("Failed to execute (%s) (%d)", cmd, e.returncode)
|
||||||
|
except OSError as e:
|
||||||
|
LOG.error("Failed to execute (%s) OS error (%d)", cmd, e.errno)
|
||||||
|
|
||||||
|
return active_device
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_local_flag_disabled(device):
|
||||||
|
"""
|
||||||
|
:param device:
|
||||||
|
:return: boolean: True if the local flag is disabled 'i.e. -clocal is
|
||||||
|
set'. This means the serial data carrier detect
|
||||||
|
signal is significant
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# uses -o for only-matching and -e for a pattern beginning with a
|
||||||
|
# hyphen (-), the following command returns 0 if the local flag
|
||||||
|
# is disabled
|
||||||
|
cmd = 'stty -a -F /dev/%s | grep -o -e -clocal' % device
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
|
||||||
|
proc.communicate()[0]
|
||||||
|
return proc.returncode == 0
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
LOG.error("Failed to execute (%s) (%d)", cmd, e.returncode)
|
||||||
|
return False
|
||||||
|
except OSError as e:
|
||||||
|
LOG.error("Failed to execute (%s) OS error (%d)", cmd, e.errno)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _config_ttys_login(self, ttys_dcd):
|
||||||
|
# agetty is now enabled by systemd
|
||||||
|
# we only need to disable the local flag to enable carrier detection
|
||||||
|
# and enable the local flag when the feature is turned off
|
||||||
|
toggle_flag = None
|
||||||
|
active_device = self._get_active_device()
|
||||||
|
local_flag_disabled = self._is_local_flag_disabled(active_device)
|
||||||
|
if str(ttys_dcd) in ['True', 'true']:
|
||||||
|
LOG.info("ttys_dcd is enabled")
|
||||||
|
# check if the local flag is disabled
|
||||||
|
if not local_flag_disabled:
|
||||||
|
LOG.info("Disable (%s) local line" % active_device)
|
||||||
|
toggle_flag = 'stty -clocal -F /dev/%s' % active_device
|
||||||
|
else:
|
||||||
|
if local_flag_disabled:
|
||||||
|
# enable local flag to ignore the carrier detection
|
||||||
|
LOG.info("Enable local flag for device :%s" % active_device)
|
||||||
|
toggle_flag = 'stty clocal -F /dev/%s' % active_device
|
||||||
|
|
||||||
|
if toggle_flag:
|
||||||
|
try:
|
||||||
|
subprocess.Popen(toggle_flag, stdout=subprocess.PIPE,
|
||||||
|
shell=True)
|
||||||
|
# restart serial-getty
|
||||||
|
restart_cmd = ('systemctl restart serial-getty@%s.service'
|
||||||
|
% active_device)
|
||||||
|
subprocess.check_call(restart_cmd, shell=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
LOG.error("subprocess error: (%d)", e.returncode)
|
||||||
|
|
||||||
|
def _force_grub_update(self):
|
||||||
|
"""Force update the grub on the first AIO controller after the initial
|
||||||
|
config is completed
|
||||||
|
"""
|
||||||
|
if (not self._first_grub_update and
|
||||||
|
os.path.isfile(tsc.INITIAL_CONFIG_COMPLETE_FLAG)):
|
||||||
|
self._first_grub_update = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def host_lldp_get_and_report(self, context, rpcapi, host_uuid):
|
||||||
|
neighbour_dict_array = []
|
||||||
|
agent_dict_array = []
|
||||||
|
neighbours = []
|
||||||
|
agents = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
neighbours = self._lldp_operator.lldp_neighbours_list()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error("Failed to get LLDP neighbours: %s", str(e))
|
||||||
|
|
||||||
|
for neighbour in neighbours:
|
||||||
|
neighbour_dict = {
|
||||||
|
'name_or_uuid': neighbour.key.portname,
|
||||||
|
'msap': neighbour.msap,
|
||||||
|
'state': neighbour.state,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_CHASSIS_ID: neighbour.key.chassisid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_ID: neighbour.key.portid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_TTL: neighbour.ttl,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_NAME: neighbour.system_name,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_DESC: neighbour.system_desc,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_CAP: neighbour.capabilities,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_MGMT_ADDR: neighbour.mgmt_addr,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_DESC: neighbour.port_desc,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_LAG: neighbour.dot1_lag,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PORT_VID: neighbour.dot1_port_vid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VID_DIGEST:
|
||||||
|
neighbour.dot1_vid_digest,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_MGMT_VID: neighbour.dot1_mgmt_vid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_VIDS:
|
||||||
|
neighbour.dot1_proto_vids,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_IDS:
|
||||||
|
neighbour.dot1_proto_ids,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VLAN_NAMES:
|
||||||
|
neighbour.dot1_vlan_names,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAC_STATUS:
|
||||||
|
neighbour.dot3_mac_status,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAX_FRAME:
|
||||||
|
neighbour.dot3_max_frame,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_POWER_MDI:
|
||||||
|
neighbour.dot3_power_mdi,
|
||||||
|
}
|
||||||
|
neighbour_dict_array.append(neighbour_dict)
|
||||||
|
|
||||||
|
if neighbour_dict_array:
|
||||||
|
try:
|
||||||
|
rpcapi.lldp_neighbour_update_by_host(context,
|
||||||
|
host_uuid,
|
||||||
|
neighbour_dict_array)
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.exception("Inventory Agent exception updating "
|
||||||
|
"lldp neighbours.")
|
||||||
|
self._lldp_operator.lldp_neighbours_clear()
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
agents = self._lldp_operator.lldp_agents_list()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error("Failed to get LLDP agents: %s", str(e))
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
agent_dict = {
|
||||||
|
'name_or_uuid': agent.key.portname,
|
||||||
|
'state': agent.state,
|
||||||
|
'status': agent.status,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_CHASSIS_ID: agent.key.chassisid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_ID: agent.key.portid,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_TTL: agent.ttl,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_NAME: agent.system_name,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_DESC: agent.system_desc,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_CAP: agent.capabilities,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_MGMT_ADDR: agent.mgmt_addr,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_DESC: agent.port_desc,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_LAG: agent.dot1_lag,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VLAN_NAMES: agent.dot1_vlan_names,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAX_FRAME: agent.dot3_max_frame,
|
||||||
|
}
|
||||||
|
agent_dict_array.append(agent_dict)
|
||||||
|
|
||||||
|
if agent_dict_array:
|
||||||
|
try:
|
||||||
|
rpcapi.lldp_agent_update_by_host(context,
|
||||||
|
host_uuid,
|
||||||
|
agent_dict_array)
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.exception("Inventory Agent exception updating "
|
||||||
|
"lldp agents.")
|
||||||
|
self._lldp_operator.lldp_agents_clear()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def synchronized_network_config(func):
|
||||||
|
"""Synchronization decorator to acquire and release
|
||||||
|
network_config_lock.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrap(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
# Get lock to avoid conflict with apply_network_config.sh
|
||||||
|
lockfd = self._acquire_network_config_lock()
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
finally:
|
||||||
|
self._release_network_config_lock(lockfd)
|
||||||
|
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
@synchronized_network_config
|
||||||
|
def _lldp_enable_and_report(self, context, rpcapi, host_uuid):
|
||||||
|
"""Temporarily enable interfaces and get lldp neighbor information.
|
||||||
|
This method should only be called before
|
||||||
|
INITIAL_CONFIG_COMPLETE_FLAG is set.
|
||||||
|
"""
|
||||||
|
links_down = []
|
||||||
|
try:
|
||||||
|
# Turn on interfaces, so that lldpd can show all neighbors
|
||||||
|
for interface in self._ipci_operator.pci_get_net_names():
|
||||||
|
flag = self._ipci_operator.pci_get_net_flags(interface)
|
||||||
|
# If administrative state is down, bring it up momentarily
|
||||||
|
if not (flag & pci.IFF_UP):
|
||||||
|
subprocess.call(['ip', 'link', 'set', interface, 'up'])
|
||||||
|
links_down.append(interface)
|
||||||
|
LOG.info('interface %s enabled to receive LLDP PDUs' %
|
||||||
|
interface)
|
||||||
|
self._lldp_operator.lldp_update()
|
||||||
|
|
||||||
|
# delay maximum 30 seconds for lldpd to receive LLDP PDU
|
||||||
|
timeout = 0
|
||||||
|
link_wait_for_lldp = True
|
||||||
|
while timeout < 30 and link_wait_for_lldp and links_down:
|
||||||
|
time.sleep(5)
|
||||||
|
timeout = timeout + 5
|
||||||
|
link_wait_for_lldp = False
|
||||||
|
|
||||||
|
for link in links_down:
|
||||||
|
if not self._lldp_operator.lldp_has_neighbour(link):
|
||||||
|
link_wait_for_lldp = True
|
||||||
|
break
|
||||||
|
self.host_lldp_get_and_report(context, rpcapi, host_uuid)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
# restore interface administrative state
|
||||||
|
for interface in links_down:
|
||||||
|
subprocess.call(['ip', 'link', 'set', interface, 'down'])
|
||||||
|
LOG.info('interface %s disabled after querying LLDP neighbors'
|
||||||
|
% interface)
|
||||||
|
|
||||||
|
def platform_update_by_host(self, rpcapi, context, host_uuid, msg_dict):
|
||||||
|
"""Update host platform information.
|
||||||
|
If this is the first boot (kickstart), then also update the Host
|
||||||
|
Action State to reinstalled, and remove the flag.
|
||||||
|
"""
|
||||||
|
if os.path.exists(FIRST_BOOT_FLAG):
|
||||||
|
msg_dict.update({k_host.HOST_ACTION_STATE:
|
||||||
|
k_host.HAS_REINSTALLED})
|
||||||
|
|
||||||
|
try:
|
||||||
|
rpcapi.platform_update_by_host(context,
|
||||||
|
host_uuid,
|
||||||
|
msg_dict)
|
||||||
|
if os.path.exists(FIRST_BOOT_FLAG):
|
||||||
|
os.remove(FIRST_BOOT_FLAG)
|
||||||
|
LOG.info("Removed %s" % FIRST_BOOT_FLAG)
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.warn("platform_update_by_host exception "
|
||||||
|
"host_uuid=%s msg_dict=%s." %
|
||||||
|
(host_uuid, msg_dict))
|
||||||
|
pass
|
||||||
|
|
||||||
|
LOG.info("Inventory Agent platform update by host: %s" % msg_dict)
|
||||||
|
|
||||||
|
def _acquire_network_config_lock(self):
|
||||||
|
"""Synchronization with apply_network_config.sh
|
||||||
|
|
||||||
|
This method is to acquire the lock to avoid
|
||||||
|
conflict with execution of apply_network_config.sh
|
||||||
|
during puppet manifest application.
|
||||||
|
|
||||||
|
:returns: fd of the lock, if successful. 0 on error.
|
||||||
|
"""
|
||||||
|
lock_file_fd = os.open(
|
||||||
|
constants.NETWORK_CONFIG_LOCK_FILE, os.O_CREAT | os.O_RDONLY)
|
||||||
|
count = 1
|
||||||
|
delay = 5
|
||||||
|
max_count = 5
|
||||||
|
while count <= max_count:
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_file_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
return lock_file_fd
|
||||||
|
except IOError as e:
|
||||||
|
# raise on unrelated IOErrors
|
||||||
|
if e.errno != errno.EAGAIN:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
LOG.info("Could not acquire lock({}): {} ({}/{}), "
|
||||||
|
"will retry".format(lock_file_fd, str(e),
|
||||||
|
count, max_count))
|
||||||
|
time.sleep(delay)
|
||||||
|
count += 1
|
||||||
|
LOG.error("Failed to acquire lock (fd={})".format(lock_file_fd))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _release_network_config_lock(self, lockfd):
|
||||||
|
"""Release the lock guarding apply_network_config.sh """
|
||||||
|
if lockfd:
|
||||||
|
fcntl.flock(lockfd, fcntl.LOCK_UN)
|
||||||
|
os.close(lockfd)
|
||||||
|
|
||||||
|
def ihost_inv_get_and_report(self, icontext):
|
||||||
|
"""Collect data for an ihost.
|
||||||
|
|
||||||
|
This method allows an ihost data to be collected.
|
||||||
|
|
||||||
|
:param: icontext: an admin context
|
||||||
|
:returns: updated ihost object, including all fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rpcapi = conductor_rpcapi.ConductorAPI(
|
||||||
|
topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
|
||||||
|
ihost = None
|
||||||
|
|
||||||
|
# find list of network related inics for this ihost
|
||||||
|
inics = self._ipci_operator.inics_get()
|
||||||
|
|
||||||
|
# create an array of ports for each net entry of the NIC device
|
||||||
|
iports = []
|
||||||
|
for inic in inics:
|
||||||
|
lockfd = 0
|
||||||
|
try:
|
||||||
|
# Get lock to avoid conflict with apply_network_config.sh
|
||||||
|
lockfd = self._acquire_network_config_lock()
|
||||||
|
pci_net_array = \
|
||||||
|
self._ipci_operator.pci_get_net_attrs(inic.pciaddr)
|
||||||
|
finally:
|
||||||
|
self._release_network_config_lock(lockfd)
|
||||||
|
for net in pci_net_array:
|
||||||
|
iports.append(pci.Port(inic, **net))
|
||||||
|
|
||||||
|
# find list of pci devices for this host
|
||||||
|
pci_devices = self._ipci_operator.pci_devices_get()
|
||||||
|
|
||||||
|
# create an array of pci_devs for each net entry of the device
|
||||||
|
pci_devs = []
|
||||||
|
for pci_dev in pci_devices:
|
||||||
|
pci_dev_array = \
|
||||||
|
self._ipci_operator.pci_get_device_attrs(pci_dev.pciaddr)
|
||||||
|
for dev in pci_dev_array:
|
||||||
|
pci_devs.append(pci.PCIDevice(pci_dev, **dev))
|
||||||
|
|
||||||
|
# create a list of MAC addresses that will be used to identify the
|
||||||
|
# inventoried host (one of the MACs should be the management MAC)
|
||||||
|
host_macs = [port.mac for port in iports if port.mac]
|
||||||
|
|
||||||
|
# get my ihost record which should be avail since booted
|
||||||
|
|
||||||
|
LOG.debug('Inventory Agent iports={}, host_macs={}'.format(
|
||||||
|
iports, host_macs))
|
||||||
|
|
||||||
|
slept = 0
|
||||||
|
while slept < MAXSLEEP:
|
||||||
|
# wait for controller to come up first may be a DOR
|
||||||
|
try:
|
||||||
|
ihost = rpcapi.get_host_by_macs(icontext, host_macs)
|
||||||
|
except messaging.MessagingTimeout:
|
||||||
|
LOG.info("get_host_by_macs Messaging Timeout.")
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.warn("Conductor RPC get_host_by_macs exception "
|
||||||
|
"response %s" % ex)
|
||||||
|
|
||||||
|
if not ihost:
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
if hostname != k_host.LOCALHOST_HOSTNAME:
|
||||||
|
try:
|
||||||
|
ihost = rpcapi.get_host_by_hostname(icontext,
|
||||||
|
hostname)
|
||||||
|
except messaging.MessagingTimeout:
|
||||||
|
LOG.info("get_host_by_hostname Messaging Timeout.")
|
||||||
|
return # wait for next audit cycle
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.warn("Conductor RPC get_host_by_hostname "
|
||||||
|
"exception response %s" % ex)
|
||||||
|
|
||||||
|
if ihost and ihost.get('personality'):
|
||||||
|
self.report_to_conductor_required = True
|
||||||
|
self._ihost_uuid = ihost['uuid']
|
||||||
|
self._ihost_personality = ihost['personality']
|
||||||
|
|
||||||
|
if os.path.isfile(tsc.PLATFORM_CONF_FILE):
|
||||||
|
# read the platform config file and check for UUID
|
||||||
|
found = False
|
||||||
|
with open(tsc.PLATFORM_CONF_FILE, "r") as fd:
|
||||||
|
for line in fd:
|
||||||
|
if line.find("UUID=") == 0:
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
# the UUID is not found, append it
|
||||||
|
with open(tsc.PLATFORM_CONF_FILE, "a") as fd:
|
||||||
|
fd.write("UUID=" + self._ihost_uuid + "\n")
|
||||||
|
|
||||||
|
# Report host install status
|
||||||
|
msg_dict = {}
|
||||||
|
self.platform_update_by_host(rpcapi,
|
||||||
|
icontext,
|
||||||
|
self._ihost_uuid,
|
||||||
|
msg_dict)
|
||||||
|
LOG.info("Agent found matching ihost: %s" % ihost['uuid'])
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(30)
|
||||||
|
slept += 30
|
||||||
|
|
||||||
|
if not self.report_to_conductor_required:
|
||||||
|
# let the audit take care of it instead
|
||||||
|
LOG.info("Inventory no matching ihost found... await Audit")
|
||||||
|
return
|
||||||
|
|
||||||
|
subfunctions = self.subfunctions_get()
|
||||||
|
try:
|
||||||
|
rpcapi.subfunctions_update_by_host(icontext,
|
||||||
|
ihost['uuid'],
|
||||||
|
subfunctions)
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.exception("Inventory Agent exception updating "
|
||||||
|
"subfunctions conductor.")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# post to inventory db by ihost['uuid']
|
||||||
|
iport_dict_array = []
|
||||||
|
for port in iports:
|
||||||
|
inic_dict = {'pciaddr': port.ipci.pciaddr,
|
||||||
|
'pclass': port.ipci.pclass,
|
||||||
|
'pvendor': port.ipci.pvendor,
|
||||||
|
'pdevice': port.ipci.pdevice,
|
||||||
|
'prevision': port.ipci.prevision,
|
||||||
|
'psvendor': port.ipci.psvendor,
|
||||||
|
'psdevice': port.ipci.psdevice,
|
||||||
|
'pname': port.name,
|
||||||
|
'numa_node': port.numa_node,
|
||||||
|
'sriov_totalvfs': port.sriov_totalvfs,
|
||||||
|
'sriov_numvfs': port.sriov_numvfs,
|
||||||
|
'sriov_vfs_pci_address': port.sriov_vfs_pci_address,
|
||||||
|
'driver': port.driver,
|
||||||
|
'mac': port.mac,
|
||||||
|
'mtu': port.mtu,
|
||||||
|
'speed': port.speed,
|
||||||
|
'link_mode': port.link_mode,
|
||||||
|
'dev_id': port.dev_id,
|
||||||
|
'dpdksupport': port.dpdksupport}
|
||||||
|
|
||||||
|
LOG.debug('Inventory Agent inic {}'.format(inic_dict))
|
||||||
|
|
||||||
|
iport_dict_array.append(inic_dict)
|
||||||
|
try:
|
||||||
|
# may get duplicate key if already sent on earlier init
|
||||||
|
rpcapi.port_update_by_host(icontext,
|
||||||
|
ihost['uuid'],
|
||||||
|
iport_dict_array)
|
||||||
|
except messaging.MessagingTimeout:
|
||||||
|
LOG.info("pci_device_update_by_host Messaging Timeout.")
|
||||||
|
self.report_to_conductor_required = False
|
||||||
|
return # wait for next audit cycle
|
||||||
|
|
||||||
|
# post to inventory db by ihost['uuid']
|
||||||
|
pci_device_dict_array = []
|
||||||
|
for dev in pci_devs:
|
||||||
|
pci_dev_dict = {'name': dev.name,
|
||||||
|
'pciaddr': dev.pci.pciaddr,
|
||||||
|
'pclass_id': dev.pclass_id,
|
||||||
|
'pvendor_id': dev.pvendor_id,
|
||||||
|
'pdevice_id': dev.pdevice_id,
|
||||||
|
'pclass': dev.pci.pclass,
|
||||||
|
'pvendor': dev.pci.pvendor,
|
||||||
|
'pdevice': dev.pci.pdevice,
|
||||||
|
'prevision': dev.pci.prevision,
|
||||||
|
'psvendor': dev.pci.psvendor,
|
||||||
|
'psdevice': dev.pci.psdevice,
|
||||||
|
'numa_node': dev.numa_node,
|
||||||
|
'sriov_totalvfs': dev.sriov_totalvfs,
|
||||||
|
'sriov_numvfs': dev.sriov_numvfs,
|
||||||
|
'sriov_vfs_pci_address': dev.sriov_vfs_pci_address,
|
||||||
|
'driver': dev.driver,
|
||||||
|
'enabled': dev.enabled,
|
||||||
|
'extra_info': dev.extra_info}
|
||||||
|
LOG.debug('Inventory Agent dev {}'.format(pci_dev_dict))
|
||||||
|
|
||||||
|
pci_device_dict_array.append(pci_dev_dict)
|
||||||
|
try:
|
||||||
|
# may get duplicate key if already sent on earlier init
|
||||||
|
rpcapi.pci_device_update_by_host(icontext,
|
||||||
|
ihost['uuid'],
|
||||||
|
pci_device_dict_array)
|
||||||
|
except messaging.MessagingTimeout:
|
||||||
|
LOG.info("pci_device_update_by_host Messaging Timeout.")
|
||||||
|
self.report_to_conductor_required = True
|
||||||
|
|
||||||
|
# Find list of numa_nodes and cpus for this ihost
|
||||||
|
inumas, icpus = self._inode_operator.inodes_get_inumas_icpus()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# may get duplicate key if already sent on earlier init
|
||||||
|
rpcapi.numas_update_by_host(icontext,
|
||||||
|
ihost['uuid'],
|
||||||
|
inumas)
|
||||||
|
except messaging.RemoteError as e:
|
||||||
|
LOG.error("numas_update_by_host RemoteError exc_type=%s" %
|
||||||
|
e.exc_type)
|
||||||
|
except messaging.MessagingTimeout:
|
||||||
|
LOG.info("pci_device_update_by_host Messaging Timeout.")
|
||||||
|
self.report_to_conductor_required = True
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("Inventory Agent exception updating inuma e=%s." % e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
force_grub_update = self._force_grub_update()
|
||||||
|
try:
|
||||||
|
# may get duplicate key if already sent on earlier init
|
||||||
|
rpcapi.cpus_update_by_host(icontext,
|
||||||
|
ihost['uuid'],
|
||||||
|
icpus,
|
||||||
|
force_grub_update)
|
||||||
|
except messaging.RemoteError as e:
|
||||||
|
LOG.error("cpus_update_by_host RemoteError exc_type=%s" %
|
||||||
|
e.exc_type)
|
||||||
|
except messaging.MessagingTimeout:
|
||||||
|
LOG.info("cpus_update_by_host Messaging Timeout.")
|
||||||
|
self.report_to_conductor_required = True
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("Inventory exception updating cpus e=%s." % e)
|
||||||
|
self.report_to_conductor_required = True
|
||||||
|
pass
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.exception("Inventory exception updating cpus conductor.")
|
||||||
|
pass
|
||||||
|
|
||||||
|
imemory = self._inode_operator.inodes_get_imemory()
|
||||||
|
if imemory:
|
||||||
|
try:
|
||||||
|
# may get duplicate key if already sent on earlier init
|
||||||
|
rpcapi.memory_update_by_host(icontext,
|
||||||
|
ihost['uuid'],
|
||||||
|
imemory)
|
||||||
|
except messaging.MessagingTimeout:
|
||||||
|
LOG.info("memory_update_by_host Messaging Timeout.")
|
||||||
|
except messaging.RemoteError as e:
|
||||||
|
LOG.error("memory_update_by_host RemoteError exc_type=%s" %
|
||||||
|
e.exc_type)
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.exception("Inventory Agent exception updating imemory "
|
||||||
|
"conductor.")
|
||||||
|
|
||||||
|
if self._ihost_uuid and \
|
||||||
|
os.path.isfile(tsc.INITIAL_CONFIG_COMPLETE_FLAG):
|
||||||
|
if not self._report_to_conductor_iplatform_avail_flag:
|
||||||
|
# and not self._wait_for_nova_lvg()
|
||||||
|
imsg_dict = {'availability': k_host.AVAILABILITY_AVAILABLE}
|
||||||
|
|
||||||
|
iscsi_initiator_name = self.get_host_iscsi_initiator_name()
|
||||||
|
if iscsi_initiator_name is not None:
|
||||||
|
imsg_dict.update({'iscsi_initiator_name':
|
||||||
|
iscsi_initiator_name})
|
||||||
|
|
||||||
|
# Before setting the host to AVAILABILITY_AVAILABLE make
|
||||||
|
# sure that nova_local aggregates are correctly set
|
||||||
|
self.platform_update_by_host(rpcapi,
|
||||||
|
icontext,
|
||||||
|
self._ihost_uuid,
|
||||||
|
imsg_dict)
|
||||||
|
|
||||||
|
self._report_to_conductor_iplatform_avail()
|
||||||
|
|
||||||
|
def subfunctions_get(self):
|
||||||
|
"""returns subfunctions on this host.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._subfunctions = ','.join(tsc.subfunctions)
|
||||||
|
|
||||||
|
return self._subfunctions
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def subfunctions_list_get():
|
||||||
|
"""returns list of subfunctions on this host.
|
||||||
|
"""
|
||||||
|
subfunctions = ','.join(tsc.subfunctions)
|
||||||
|
subfunctions_list = subfunctions.split(',')
|
||||||
|
|
||||||
|
return subfunctions_list
|
||||||
|
|
||||||
|
def subfunctions_configured(self, subfunctions_list):
|
||||||
|
"""Determines whether subfunctions configuration is completed.
|
||||||
|
return: Bool whether subfunctions configuration is completed.
|
||||||
|
"""
|
||||||
|
if (k_host.CONTROLLER in subfunctions_list and
|
||||||
|
k_host.COMPUTE in subfunctions_list):
|
||||||
|
if not os.path.exists(tsc.INITIAL_COMPUTE_CONFIG_COMPLETE):
|
||||||
|
self._subfunctions_configured = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._subfunctions_configured = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _wait_for_nova_lvg(icontext, rpcapi, ihost_uuid, nova_lvgs=None):
|
||||||
|
"""See if we wait for a provisioned nova-local volume group
|
||||||
|
|
||||||
|
This method queries the conductor to see if we are provisioning
|
||||||
|
a nova-local volume group on this boot cycle. This check is used
|
||||||
|
to delay sending the platform availability to the conductor.
|
||||||
|
|
||||||
|
:param: icontext: an admin context
|
||||||
|
:param: rpcapi: conductor rpc api
|
||||||
|
:param: ihost_uuid: an admin context
|
||||||
|
:returns: True if we are provisioning false otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
return True
|
||||||
|
LOG.info("TODO _wait_for_nova_lvg from systemconfig")
|
||||||
|
|
||||||
|
def _is_config_complete(self):
|
||||||
|
"""Check if this node has completed config
|
||||||
|
|
||||||
|
This method queries node's config flag file to see if it has
|
||||||
|
complete config.
|
||||||
|
:return: True if the complete flag file exists false otherwise
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(tsc.INITIAL_CONFIG_COMPLETE_FLAG):
|
||||||
|
return False
|
||||||
|
subfunctions = self.subfunctions_list_get()
|
||||||
|
if k_host.CONTROLLER in subfunctions:
|
||||||
|
if not os.path.isfile(tsc.INITIAL_CONTROLLER_CONFIG_COMPLETE):
|
||||||
|
return False
|
||||||
|
if k_host.COMPUTE in subfunctions:
|
||||||
|
if not os.path.isfile(tsc.INITIAL_COMPUTE_CONFIG_COMPLETE):
|
||||||
|
return False
|
||||||
|
if k_host.STORAGE in subfunctions:
|
||||||
|
if not os.path.isfile(tsc.INITIAL_STORAGE_CONFIG_COMPLETE):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@periodics.periodic(spacing=CONF.agent.audit_interval,
|
||||||
|
run_immediately=True)
|
||||||
|
def _agent_audit(self, context):
|
||||||
|
# periodically, perform inventory audit
|
||||||
|
self.agent_audit(context, host_uuid=self._ihost_uuid,
|
||||||
|
force_updates=None)
|
||||||
|
|
||||||
|
def agent_audit(self, context,
|
||||||
|
host_uuid, force_updates, cinder_device=None):
|
||||||
|
# perform inventory audit
|
||||||
|
if self._ihost_uuid != host_uuid:
|
||||||
|
# The function call is not for this host agent
|
||||||
|
return
|
||||||
|
|
||||||
|
icontext = mycontext.get_admin_context()
|
||||||
|
rpcapi = conductor_rpcapi.ConductorAPI(
|
||||||
|
topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
|
||||||
|
if not self.report_to_conductor_required:
|
||||||
|
LOG.info("Inventory Agent audit running inv_get_and_report.")
|
||||||
|
self.ihost_inv_get_and_report(icontext)
|
||||||
|
|
||||||
|
if self._ihost_uuid and os.path.isfile(
|
||||||
|
tsc.INITIAL_CONFIG_COMPLETE_FLAG):
|
||||||
|
if (not self._report_to_conductor_iplatform_avail_flag and
|
||||||
|
not self._wait_for_nova_lvg(
|
||||||
|
icontext, rpcapi, self._ihost_uuid)):
|
||||||
|
imsg_dict = {'availability': k_host.AVAILABILITY_AVAILABLE}
|
||||||
|
|
||||||
|
iscsi_initiator_name = self.get_host_iscsi_initiator_name()
|
||||||
|
if iscsi_initiator_name is not None:
|
||||||
|
imsg_dict.update({'iscsi_initiator_name':
|
||||||
|
iscsi_initiator_name})
|
||||||
|
|
||||||
|
# Before setting the host to AVAILABILITY_AVAILABLE make
|
||||||
|
# sure that nova_local aggregates are correctly set
|
||||||
|
self.platform_update_by_host(rpcapi,
|
||||||
|
icontext,
|
||||||
|
self._ihost_uuid,
|
||||||
|
imsg_dict)
|
||||||
|
|
||||||
|
self._report_to_conductor_iplatform_avail()
|
||||||
|
|
||||||
|
if (self._ihost_personality == k_host.CONTROLLER and
|
||||||
|
not self._notify_subfunctions_alarm_clear):
|
||||||
|
|
||||||
|
subfunctions_list = self.subfunctions_list_get()
|
||||||
|
if ((k_host.CONTROLLER in subfunctions_list) and
|
||||||
|
(k_host.COMPUTE in subfunctions_list)):
|
||||||
|
if self.subfunctions_configured(subfunctions_list) and \
|
||||||
|
not self._wait_for_nova_lvg(
|
||||||
|
icontext, rpcapi, self._ihost_uuid):
|
||||||
|
|
||||||
|
ihost_notify_dict = {'subfunctions_configured': True}
|
||||||
|
rpcapi.notify_subfunctions_config(icontext,
|
||||||
|
self._ihost_uuid,
|
||||||
|
ihost_notify_dict)
|
||||||
|
self._notify_subfunctions_alarm_clear = True
|
||||||
|
else:
|
||||||
|
if not self._notify_subfunctions_alarm_raise:
|
||||||
|
ihost_notify_dict = {'subfunctions_configured':
|
||||||
|
False}
|
||||||
|
rpcapi.notify_subfunctions_config(
|
||||||
|
icontext, self._ihost_uuid, ihost_notify_dict)
|
||||||
|
self._notify_subfunctions_alarm_raise = True
|
||||||
|
else:
|
||||||
|
self._notify_subfunctions_alarm_clear = True
|
||||||
|
|
||||||
|
if self._ihost_uuid:
|
||||||
|
LOG.debug("Inventory Agent Audit running.")
|
||||||
|
|
||||||
|
if force_updates:
|
||||||
|
LOG.debug("Inventory Agent Audit force updates: (%s)" %
|
||||||
|
(', '.join(force_updates)))
|
||||||
|
|
||||||
|
self._update_ttys_dcd_status(icontext, self._ihost_uuid)
|
||||||
|
if self._agent_throttle > 5:
|
||||||
|
# throttle updates
|
||||||
|
self._agent_throttle = 0
|
||||||
|
imemory = self._inode_operator.inodes_get_imemory()
|
||||||
|
rpcapi.memory_update_by_host(icontext,
|
||||||
|
self._ihost_uuid,
|
||||||
|
imemory)
|
||||||
|
if self._is_config_complete():
|
||||||
|
self.host_lldp_get_and_report(
|
||||||
|
icontext, rpcapi, self._ihost_uuid)
|
||||||
|
else:
|
||||||
|
self._lldp_enable_and_report(
|
||||||
|
icontext, rpcapi, self._ihost_uuid)
|
||||||
|
self._agent_throttle += 1
|
||||||
|
|
||||||
|
if os.path.isfile(tsc.PLATFORM_CONF_FILE):
|
||||||
|
# read the platform config file and check for UUID
|
||||||
|
if 'UUID' not in open(tsc.PLATFORM_CONF_FILE).read():
|
||||||
|
# the UUID is not in found, append it
|
||||||
|
with open(tsc.PLATFORM_CONF_FILE, "a") as fd:
|
||||||
|
fd.write("UUID=" + self._ihost_uuid)
|
||||||
|
|
||||||
|
def configure_lldp_systemname(self, context, systemname):
|
||||||
|
"""Configure the systemname into the lldp agent with the supplied data.
|
||||||
|
|
||||||
|
:param context: an admin context.
|
||||||
|
:param systemname: the systemname
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO(sc): This becomes an inventory-api call from
|
||||||
|
# via systemconfig: configure_isystemname
|
||||||
|
rpcapi = conductor_rpcapi.ConductorAPI(
|
||||||
|
topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
# Update the lldp agent
|
||||||
|
self._lldp_operator.lldp_update_systemname(systemname)
|
||||||
|
# Trigger an audit to ensure the db is up to date
|
||||||
|
self.host_lldp_get_and_report(context, rpcapi, self._ihost_uuid)
|
||||||
|
|
||||||
|
def configure_ttys_dcd(self, context, uuid, ttys_dcd):
|
||||||
|
"""Configure the getty on the serial device.
|
||||||
|
|
||||||
|
:param context: an admin context.
|
||||||
|
:param uuid: the host uuid
|
||||||
|
:param ttys_dcd: the flag to enable/disable dcd
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOG.debug("AgentManager.configure_ttys_dcd: %s %s" % (uuid, ttys_dcd))
|
||||||
|
if self._ihost_uuid and self._ihost_uuid == uuid:
|
||||||
|
LOG.debug("AgentManager configure getty on serial console")
|
||||||
|
self._config_ttys_login(ttys_dcd)
|
||||||
|
return
|
||||||
|
|
||||||
|
def execute_command(self, context, host_uuid, command):
|
||||||
|
"""Execute a command on behalf of inventory-conductor
|
||||||
|
|
||||||
|
:param context: request context
|
||||||
|
:param host_uuid: the host uuid
|
||||||
|
:param command: the command to execute
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOG.debug("AgentManager.execute_command: (%s)" % command)
|
||||||
|
if self._ihost_uuid and self._ihost_uuid == host_uuid:
|
||||||
|
LOG.info("AgentManager execute_command: (%s)" % command)
|
||||||
|
with open(os.devnull, "w") as fnull:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(command, stdout=fnull, stderr=fnull)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
LOG.error("Failed to execute (%s) (%d)",
|
||||||
|
command, e.returncode)
|
||||||
|
except OSError as e:
|
||||||
|
LOG.error("Failed to execute (%s), OS error:(%d)",
|
||||||
|
command, e.errno)
|
||||||
|
|
||||||
|
LOG.info("(%s) executed.", command)
|
||||||
|
|
||||||
|
def get_host_iscsi_initiator_name(self):
|
||||||
|
iscsi_initiator_name = None
|
||||||
|
try:
|
||||||
|
stdout, __ = utils.execute('cat', '/etc/iscsi/initiatorname.iscsi',
|
||||||
|
run_as_root=True)
|
||||||
|
if stdout:
|
||||||
|
stdout = stdout.strip()
|
||||||
|
iscsi_initiator_name = stdout.split('=')[-1]
|
||||||
|
LOG.info("iscsi initiator name = %s" % iscsi_initiator_name)
|
||||||
|
except Exception:
|
||||||
|
LOG.error("Failed retrieving iscsi initiator name")
|
||||||
|
|
||||||
|
return iscsi_initiator_name
|
||||||
|
|
||||||
|
def update_host_memory(self, context, host_uuid):
|
||||||
|
"""update the host memory
|
||||||
|
|
||||||
|
:param context: an admin context
|
||||||
|
:param host_uuid: ihost uuid unique id
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if self._ihost_uuid and self._ihost_uuid == host_uuid:
|
||||||
|
rpcapi = conductor_rpcapi.ConductorAPI(
|
||||||
|
topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
memory = self._inode_operator.inodes_get_imemory()
|
||||||
|
rpcapi.memory_update_by_host(context,
|
||||||
|
self._ihost_uuid,
|
||||||
|
memory,
|
||||||
|
force_update=True)
|
|
@ -0,0 +1,608 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
""" inventory numa node Utilities and helper functions."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from os import listdir
|
||||||
|
from os.path import isfile
|
||||||
|
from os.path import join
|
||||||
|
from oslo_log import log
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tsconfig.tsconfig as tsc
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
# Defines per-socket vswitch memory requirements (in MB)
|
||||||
|
VSWITCH_MEMORY_MB = 1024
|
||||||
|
|
||||||
|
# Defines the size of one kilobyte
|
||||||
|
SIZE_KB = 1024
|
||||||
|
|
||||||
|
# Defines the size of 2 megabytes in kilobyte units
|
||||||
|
SIZE_2M_KB = 2048
|
||||||
|
|
||||||
|
# Defines the size of 1 gigabyte in kilobyte units
|
||||||
|
SIZE_1G_KB = 1048576
|
||||||
|
|
||||||
|
# Defines the size of 2 megabytes in megabyte units
|
||||||
|
SIZE_2M_MB = int(SIZE_2M_KB / SIZE_KB)
|
||||||
|
|
||||||
|
# Defines the size of 1 gigabyte in megabyte units
|
||||||
|
SIZE_1G_MB = int(SIZE_1G_KB / SIZE_KB)
|
||||||
|
|
||||||
|
# Defines the minimum size of memory for a controller node in megabyte units
|
||||||
|
CONTROLLER_MIN_MB = 6000
|
||||||
|
|
||||||
|
# Defines the minimum size of memory for a compute node in megabyte units
|
||||||
|
COMPUTE_MIN_MB = 1600
|
||||||
|
|
||||||
|
# Defines the minimum size of memory for a secondary compute node in megabyte
|
||||||
|
# units
|
||||||
|
COMPUTE_MIN_NON_0_MB = 500
|
||||||
|
|
||||||
|
|
||||||
|
class CPU(object):
|
||||||
|
'''Class to encapsulate CPU data for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self, cpu, numa_node, core, thread,
|
||||||
|
cpu_family=None, cpu_model=None, revision=None):
|
||||||
|
'''Construct a cpu object with the given values.'''
|
||||||
|
|
||||||
|
self.cpu = cpu
|
||||||
|
self.numa_node = numa_node
|
||||||
|
self.core = core
|
||||||
|
self.thread = thread
|
||||||
|
self.cpu_family = cpu_family
|
||||||
|
self.cpu_model = cpu_model
|
||||||
|
self.revision = revision
|
||||||
|
# self.allocated_functions = mgmt (usu. 0), vswitch
|
||||||
|
|
||||||
|
def __eq__(self, rhs):
|
||||||
|
return (self.cpu == rhs.cpu and
|
||||||
|
self.numa_node == rhs.numa_node and
|
||||||
|
self.core == rhs.core and
|
||||||
|
self.thread == rhs.thread)
|
||||||
|
|
||||||
|
def __ne__(self, rhs):
|
||||||
|
return (self.cpu != rhs.cpu or
|
||||||
|
self.numa_node != rhs.numa_node or
|
||||||
|
self.core != rhs.core or
|
||||||
|
self.thread != rhs.thread)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s [%s] [%s] [%s]" % (self.cpu, self.numa_node,
|
||||||
|
self.core, self.thread)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<CPU '%s'>" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeOperator(object):
|
||||||
|
'''Class to encapsulate CPU operations for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.num_cpus = 0
|
||||||
|
self.num_nodes = 0
|
||||||
|
self.float_cpuset = 0
|
||||||
|
self.total_memory_mb = 0
|
||||||
|
self.free_memory_mb = 0
|
||||||
|
self.total_memory_nodes_mb = []
|
||||||
|
self.free_memory_nodes_mb = []
|
||||||
|
self.topology = {}
|
||||||
|
|
||||||
|
# self._get_cpu_topology()
|
||||||
|
# self._get_total_memory_mb()
|
||||||
|
# self._get_total_memory_nodes_mb()
|
||||||
|
# self._get_free_memory_mb()
|
||||||
|
# self._get_free_memory_nodes_mb()
|
||||||
|
|
||||||
|
def _is_strict(self):
|
||||||
|
with open(os.devnull, "w") as fnull:
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
["cat", "/proc/sys/vm/overcommit_memory"],
|
||||||
|
stderr=fnull)
|
||||||
|
if int(output) == 2:
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
LOG.info("Failed to check for overcommit, error (%s)",
|
||||||
|
e.output)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def convert_range_string_to_list(self, s):
|
||||||
|
olist = []
|
||||||
|
s = s.strip()
|
||||||
|
if s:
|
||||||
|
for part in s.split(','):
|
||||||
|
if '-' in part:
|
||||||
|
a, b = part.split('-')
|
||||||
|
a, b = int(a), int(b)
|
||||||
|
olist.extend(range(a, b + 1))
|
||||||
|
else:
|
||||||
|
a = int(part)
|
||||||
|
olist.append(a)
|
||||||
|
olist.sort()
|
||||||
|
return olist
|
||||||
|
|
||||||
|
def inodes_get_inumas_icpus(self):
|
||||||
|
'''Enumerate logical cpu topology based on parsing /proc/cpuinfo
|
||||||
|
as function of socket_id, core_id, and thread_id. This updates
|
||||||
|
topology.
|
||||||
|
|
||||||
|
:param self
|
||||||
|
:updates self.num_cpus- number of logical cpus
|
||||||
|
:updates self.num_nodes- number of sockets;maps to number of numa nodes
|
||||||
|
:updates self.topology[socket_id][core_id][thread_id] = cpu
|
||||||
|
:returns None
|
||||||
|
'''
|
||||||
|
self.num_cpus = 0
|
||||||
|
self.num_nodes = 0
|
||||||
|
self.topology = {}
|
||||||
|
|
||||||
|
thread_cnt = {}
|
||||||
|
cpu = socket_id = core_id = thread_id = -1
|
||||||
|
re_processor = re.compile(r'^[Pp]rocessor\s+:\s+(\d+)')
|
||||||
|
re_socket = re.compile(r'^physical id\s+:\s+(\d+)')
|
||||||
|
re_core = re.compile(r'^core id\s+:\s+(\d+)')
|
||||||
|
re_cpu_family = re.compile(r'^cpu family\s+:\s+(\d+)')
|
||||||
|
re_cpu_model = re.compile(r'^model name\s+:\s+(\w+)')
|
||||||
|
|
||||||
|
inumas = []
|
||||||
|
icpus = []
|
||||||
|
sockets = []
|
||||||
|
|
||||||
|
with open('/proc/cpuinfo', 'r') as infile:
|
||||||
|
icpu_attrs = {}
|
||||||
|
|
||||||
|
for line in infile:
|
||||||
|
match = re_processor.search(line)
|
||||||
|
if match:
|
||||||
|
cpu = int(match.group(1))
|
||||||
|
socket_id = -1
|
||||||
|
core_id = -1
|
||||||
|
thread_id = -1
|
||||||
|
self.num_cpus += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = re_cpu_family.search(line)
|
||||||
|
if match:
|
||||||
|
name_value = [s.strip() for s in line.split(':', 1)]
|
||||||
|
name, value = name_value
|
||||||
|
icpu_attrs.update({'cpu_family': value})
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = re_cpu_model.search(line)
|
||||||
|
if match:
|
||||||
|
name_value = [s.strip() for s in line.split(':', 1)]
|
||||||
|
name, value = name_value
|
||||||
|
icpu_attrs.update({'cpu_model': value})
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = re_socket.search(line)
|
||||||
|
if match:
|
||||||
|
socket_id = int(match.group(1))
|
||||||
|
if socket_id not in sockets:
|
||||||
|
sockets.append(socket_id)
|
||||||
|
attrs = {
|
||||||
|
'numa_node': socket_id,
|
||||||
|
'capabilities': {},
|
||||||
|
}
|
||||||
|
inumas.append(attrs)
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = re_core.search(line)
|
||||||
|
if match:
|
||||||
|
core_id = int(match.group(1))
|
||||||
|
|
||||||
|
if socket_id not in thread_cnt:
|
||||||
|
thread_cnt[socket_id] = {}
|
||||||
|
if core_id not in thread_cnt[socket_id]:
|
||||||
|
thread_cnt[socket_id][core_id] = 0
|
||||||
|
else:
|
||||||
|
thread_cnt[socket_id][core_id] += 1
|
||||||
|
thread_id = thread_cnt[socket_id][core_id]
|
||||||
|
|
||||||
|
if socket_id not in self.topology:
|
||||||
|
self.topology[socket_id] = {}
|
||||||
|
if core_id not in self.topology[socket_id]:
|
||||||
|
self.topology[socket_id][core_id] = {}
|
||||||
|
|
||||||
|
self.topology[socket_id][core_id][thread_id] = cpu
|
||||||
|
attrs = {
|
||||||
|
'cpu': cpu,
|
||||||
|
'numa_node': socket_id,
|
||||||
|
'core': core_id,
|
||||||
|
'thread': thread_id,
|
||||||
|
'capabilities': {},
|
||||||
|
}
|
||||||
|
icpu_attrs.update(attrs)
|
||||||
|
icpus.append(icpu_attrs)
|
||||||
|
icpu_attrs = {}
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.num_nodes = len(self.topology.keys())
|
||||||
|
|
||||||
|
# In the case topology not detected, hard-code structures
|
||||||
|
if self.num_nodes == 0:
|
||||||
|
n_sockets, n_cores, n_threads = (1, int(self.num_cpus), 1)
|
||||||
|
self.topology = {}
|
||||||
|
for socket_id in range(n_sockets):
|
||||||
|
self.topology[socket_id] = {}
|
||||||
|
if socket_id not in sockets:
|
||||||
|
sockets.append(socket_id)
|
||||||
|
attrs = {
|
||||||
|
'numa_node': socket_id,
|
||||||
|
'capabilities': {},
|
||||||
|
}
|
||||||
|
inumas.append(attrs)
|
||||||
|
for core_id in range(n_cores):
|
||||||
|
self.topology[socket_id][core_id] = {}
|
||||||
|
for thread_id in range(n_threads):
|
||||||
|
self.topology[socket_id][core_id][thread_id] = 0
|
||||||
|
attrs = {
|
||||||
|
'cpu': cpu,
|
||||||
|
'numa_node': socket_id,
|
||||||
|
'core': core_id,
|
||||||
|
'thread': thread_id,
|
||||||
|
'capabilities': {},
|
||||||
|
}
|
||||||
|
icpus.append(attrs)
|
||||||
|
|
||||||
|
# Define Thread-Socket-Core order for logical cpu enumeration
|
||||||
|
cpu = 0
|
||||||
|
for thread_id in range(n_threads):
|
||||||
|
for core_id in range(n_cores):
|
||||||
|
for socket_id in range(n_sockets):
|
||||||
|
if socket_id not in sockets:
|
||||||
|
sockets.append(socket_id)
|
||||||
|
attrs = {
|
||||||
|
'numa_node': socket_id,
|
||||||
|
'capabilities': {},
|
||||||
|
}
|
||||||
|
inumas.append(attrs)
|
||||||
|
self.topology[socket_id][core_id][thread_id] = cpu
|
||||||
|
attrs = {
|
||||||
|
'cpu': cpu,
|
||||||
|
'numa_node': socket_id,
|
||||||
|
'core': core_id,
|
||||||
|
'thread': thread_id,
|
||||||
|
'capabilities': {},
|
||||||
|
}
|
||||||
|
icpus.append(attrs)
|
||||||
|
cpu += 1
|
||||||
|
self.num_nodes = len(self.topology.keys())
|
||||||
|
|
||||||
|
LOG.debug("inumas= %s, cpus = %s" % (inumas, icpus))
|
||||||
|
|
||||||
|
return inumas, icpus
|
||||||
|
|
||||||
|
def _get_immediate_subdirs(self, dir):
|
||||||
|
return [name for name in listdir(dir)
|
||||||
|
if os.path.isdir(join(dir, name))]
|
||||||
|
|
||||||
|
def _inode_get_memory_hugepages(self):
|
||||||
|
"""Collect hugepage info, including vswitch, and vm.
|
||||||
|
Collect platform reserved if config.
|
||||||
|
:param self
|
||||||
|
:returns list of memory nodes and attributes
|
||||||
|
"""
|
||||||
|
|
||||||
|
imemory = []
|
||||||
|
|
||||||
|
initial_compute_config_completed = \
|
||||||
|
os.path.exists(tsc.INITIAL_COMPUTE_CONFIG_COMPLETE)
|
||||||
|
|
||||||
|
# check if it is initial report before the huge pages are allocated
|
||||||
|
initial_report = not initial_compute_config_completed
|
||||||
|
|
||||||
|
# do not send report if the initial compute config is completed and
|
||||||
|
# compute config has not finished, i.e.during subsequent
|
||||||
|
# reboot before the manifest allocates the huge pages
|
||||||
|
compute_config_completed = \
|
||||||
|
os.path.exists(tsc.VOLATILE_COMPUTE_CONFIG_COMPLETE)
|
||||||
|
if (initial_compute_config_completed and
|
||||||
|
not compute_config_completed):
|
||||||
|
return imemory
|
||||||
|
|
||||||
|
for node in range(self.num_nodes):
|
||||||
|
attr = {}
|
||||||
|
total_hp_mb = 0 # Total memory (MB) currently configured in HPs
|
||||||
|
free_hp_mb = 0
|
||||||
|
|
||||||
|
# Check vswitch and libvirt memory
|
||||||
|
# Loop through configured hugepage sizes of this node and record
|
||||||
|
# total number and number free
|
||||||
|
hugepages = "/sys/devices/system/node/node%d/hugepages" % node
|
||||||
|
|
||||||
|
try:
|
||||||
|
subdirs = self._get_immediate_subdirs(hugepages)
|
||||||
|
|
||||||
|
for subdir in subdirs:
|
||||||
|
hp_attr = {}
|
||||||
|
sizesplit = subdir.split('-')
|
||||||
|
if sizesplit[1].startswith("1048576kB"):
|
||||||
|
size = SIZE_1G_MB
|
||||||
|
else:
|
||||||
|
size = SIZE_2M_MB
|
||||||
|
|
||||||
|
nr_hugepages = 0
|
||||||
|
free_hugepages = 0
|
||||||
|
|
||||||
|
mydir = hugepages + '/' + subdir
|
||||||
|
files = [f for f in listdir(mydir)
|
||||||
|
if isfile(join(mydir, f))]
|
||||||
|
|
||||||
|
if files:
|
||||||
|
for file in files:
|
||||||
|
with open(mydir + '/' + file, 'r') as f:
|
||||||
|
if file.startswith("nr_hugepages"):
|
||||||
|
nr_hugepages = int(f.readline())
|
||||||
|
if file.startswith("free_hugepages"):
|
||||||
|
free_hugepages = int(f.readline())
|
||||||
|
|
||||||
|
total_hp_mb = total_hp_mb + int(nr_hugepages * size)
|
||||||
|
free_hp_mb = free_hp_mb + int(free_hugepages * size)
|
||||||
|
|
||||||
|
# Libvirt hugepages can be 1G and 2M
|
||||||
|
if size == SIZE_1G_MB:
|
||||||
|
vswitch_hugepages_nr = VSWITCH_MEMORY_MB / size
|
||||||
|
hp_attr = {
|
||||||
|
'vswitch_hugepages_size_mib': size,
|
||||||
|
'vswitch_hugepages_nr': vswitch_hugepages_nr,
|
||||||
|
'vswitch_hugepages_avail': 0,
|
||||||
|
'vm_hugepages_nr_1G':
|
||||||
|
(nr_hugepages - vswitch_hugepages_nr),
|
||||||
|
'vm_hugepages_avail_1G': free_hugepages,
|
||||||
|
'vm_hugepages_use_1G': 'True'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if len(subdirs) == 1:
|
||||||
|
# No 1G hugepage support.
|
||||||
|
vswitch_hugepages_nr = VSWITCH_MEMORY_MB / size
|
||||||
|
hp_attr = {
|
||||||
|
'vswitch_hugepages_size_mib': size,
|
||||||
|
'vswitch_hugepages_nr': vswitch_hugepages_nr,
|
||||||
|
'vswitch_hugepages_avail': 0,
|
||||||
|
}
|
||||||
|
hp_attr.update({'vm_hugepages_use_1G': 'False'})
|
||||||
|
else:
|
||||||
|
# vswitch will use 1G hugpages
|
||||||
|
vswitch_hugepages_nr = 0
|
||||||
|
|
||||||
|
hp_attr.update({
|
||||||
|
'vm_hugepages_avail_2M': free_hugepages,
|
||||||
|
'vm_hugepages_nr_2M':
|
||||||
|
(nr_hugepages - vswitch_hugepages_nr)
|
||||||
|
})
|
||||||
|
|
||||||
|
attr.update(hp_attr)
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
# silently ignore IO errors (eg. file missing)
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get the free and total memory from meminfo for this node
|
||||||
|
re_node_memtotal = re.compile(r'^Node\s+\d+\s+\MemTotal:\s+(\d+)')
|
||||||
|
re_node_memfree = re.compile(r'^Node\s+\d+\s+\MemFree:\s+(\d+)')
|
||||||
|
re_node_filepages = \
|
||||||
|
re.compile(r'^Node\s+\d+\s+\FilePages:\s+(\d+)')
|
||||||
|
re_node_sreclaim = \
|
||||||
|
re.compile(r'^Node\s+\d+\s+\SReclaimable:\s+(\d+)')
|
||||||
|
re_node_commitlimit = \
|
||||||
|
re.compile(r'^Node\s+\d+\s+\CommitLimit:\s+(\d+)')
|
||||||
|
re_node_committed_as = \
|
||||||
|
re.compile(r'^Node\s+\d+\s+\'Committed_AS:\s+(\d+)')
|
||||||
|
|
||||||
|
free_kb = 0 # Free Memory (KB) available
|
||||||
|
total_kb = 0 # Total Memory (KB)
|
||||||
|
limit = 0 # only used in strict accounting
|
||||||
|
committed = 0 # only used in strict accounting
|
||||||
|
|
||||||
|
meminfo = "/sys/devices/system/node/node%d/meminfo" % node
|
||||||
|
try:
|
||||||
|
with open(meminfo, 'r') as infile:
|
||||||
|
for line in infile:
|
||||||
|
match = re_node_memtotal.search(line)
|
||||||
|
if match:
|
||||||
|
total_kb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
match = re_node_memfree.search(line)
|
||||||
|
if match:
|
||||||
|
free_kb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
match = re_node_filepages.search(line)
|
||||||
|
if match:
|
||||||
|
free_kb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
match = re_node_sreclaim.search(line)
|
||||||
|
if match:
|
||||||
|
free_kb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
match = re_node_commitlimit.search(line)
|
||||||
|
if match:
|
||||||
|
limit = int(match.group(1))
|
||||||
|
continue
|
||||||
|
match = re_node_committed_as.search(line)
|
||||||
|
if match:
|
||||||
|
committed = int(match.group(1))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._is_strict():
|
||||||
|
free_kb = limit - committed
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
# silently ignore IO errors (eg. file missing)
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Calculate PSS
|
||||||
|
pss_mb = 0
|
||||||
|
if node == 0:
|
||||||
|
cmd = 'cat /proc/*/smaps 2>/dev/null | awk \'/^Pss:/ ' \
|
||||||
|
'{a += $2;} END {printf "%d\\n", a/1024.0;}\''
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||||
|
shell=True)
|
||||||
|
result = proc.stdout.read().strip()
|
||||||
|
pss_mb = int(result)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
LOG.error("Cannot calculate PSS (%s) (%d)", cmd,
|
||||||
|
e.returncode)
|
||||||
|
except OSError as e:
|
||||||
|
LOG.error("Failed to execute (%s) OS error (%d)", cmd,
|
||||||
|
e.errno)
|
||||||
|
|
||||||
|
# need to multiply total_mb by 1024 to match compute_huge
|
||||||
|
node_total_kb = total_hp_mb * SIZE_KB + free_kb + pss_mb * SIZE_KB
|
||||||
|
|
||||||
|
# Read base memory from compute_reserved.conf
|
||||||
|
base_mem_mb = 0
|
||||||
|
with open('/etc/nova/compute_reserved.conf', 'r') as infile:
|
||||||
|
for line in infile:
|
||||||
|
if "COMPUTE_BASE_RESERVED" in line:
|
||||||
|
val = line.split("=")
|
||||||
|
base_reserves = val[1].strip('\n')[1:-1]
|
||||||
|
for reserve in base_reserves.split():
|
||||||
|
reserve = reserve.split(":")
|
||||||
|
if reserve[0].strip('"') == "node%d" % node:
|
||||||
|
base_mem_mb = int(reserve[1].strip('MB'))
|
||||||
|
|
||||||
|
# On small systems, clip memory overhead to more reasonable minimal
|
||||||
|
# settings
|
||||||
|
if (total_kb / SIZE_KB - base_mem_mb) < 1000:
|
||||||
|
if node == 0:
|
||||||
|
base_mem_mb = COMPUTE_MIN_MB
|
||||||
|
if tsc.nodetype == 'controller':
|
||||||
|
base_mem_mb += CONTROLLER_MIN_MB
|
||||||
|
else:
|
||||||
|
base_mem_mb = COMPUTE_MIN_NON_0_MB
|
||||||
|
|
||||||
|
eng_kb = node_total_kb - base_mem_mb * SIZE_KB
|
||||||
|
|
||||||
|
vswitch_mem_kb = (attr.get('vswitch_hugepages_size_mib', 0) *
|
||||||
|
attr.get('vswitch_hugepages_nr', 0) * SIZE_KB)
|
||||||
|
|
||||||
|
vm_kb = (eng_kb - vswitch_mem_kb)
|
||||||
|
|
||||||
|
max_vm_pages_2mb = vm_kb / SIZE_2M_KB
|
||||||
|
max_vm_pages_1gb = vm_kb / SIZE_1G_KB
|
||||||
|
|
||||||
|
attr.update({
|
||||||
|
'vm_hugepages_possible_2M': max_vm_pages_2mb,
|
||||||
|
'vm_hugepages_possible_1G': max_vm_pages_1gb,
|
||||||
|
})
|
||||||
|
|
||||||
|
# calculate 90% 2M pages if it is initial report and the huge
|
||||||
|
# pages have not been allocated
|
||||||
|
if initial_report:
|
||||||
|
max_vm_pages_2mb = max_vm_pages_2mb * 0.9
|
||||||
|
total_hp_mb += int(max_vm_pages_2mb * (SIZE_2M_KB / SIZE_KB))
|
||||||
|
free_hp_mb = total_hp_mb
|
||||||
|
attr.update({
|
||||||
|
'vm_hugepages_nr_2M': max_vm_pages_2mb,
|
||||||
|
'vm_hugepages_avail_2M': max_vm_pages_2mb,
|
||||||
|
'vm_hugepages_nr_1G': 0
|
||||||
|
})
|
||||||
|
|
||||||
|
attr.update({
|
||||||
|
'numa_node': node,
|
||||||
|
'memtotal_mib': total_hp_mb,
|
||||||
|
'memavail_mib': free_hp_mb,
|
||||||
|
'hugepages_configured': 'True',
|
||||||
|
'node_memtotal_mib': node_total_kb / 1024,
|
||||||
|
})
|
||||||
|
|
||||||
|
imemory.append(attr)
|
||||||
|
|
||||||
|
return imemory
|
||||||
|
|
||||||
|
def _inode_get_memory_nonhugepages(self):
|
||||||
|
'''Collect nonhugepage info, including platform reserved if config.
|
||||||
|
:param self
|
||||||
|
:returns list of memory nodes and attributes
|
||||||
|
'''
|
||||||
|
|
||||||
|
imemory = []
|
||||||
|
self.total_memory_mb = 0
|
||||||
|
|
||||||
|
re_node_memtotal = re.compile(r'^Node\s+\d+\s+\MemTotal:\s+(\d+)')
|
||||||
|
re_node_memfree = re.compile(r'^Node\s+\d+\s+\MemFree:\s+(\d+)')
|
||||||
|
re_node_filepages = re.compile(r'^Node\s+\d+\s+\FilePages:\s+(\d+)')
|
||||||
|
re_node_sreclaim = re.compile(r'^Node\s+\d+\s+\SReclaimable:\s+(\d+)')
|
||||||
|
|
||||||
|
for node in range(self.num_nodes):
|
||||||
|
attr = {}
|
||||||
|
total_mb = 0
|
||||||
|
free_mb = 0
|
||||||
|
|
||||||
|
meminfo = "/sys/devices/system/node/node%d/meminfo" % node
|
||||||
|
try:
|
||||||
|
with open(meminfo, 'r') as infile:
|
||||||
|
for line in infile:
|
||||||
|
match = re_node_memtotal.search(line)
|
||||||
|
if match:
|
||||||
|
total_mb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = re_node_memfree.search(line)
|
||||||
|
if match:
|
||||||
|
free_mb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
match = re_node_filepages.search(line)
|
||||||
|
if match:
|
||||||
|
free_mb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
match = re_node_sreclaim.search(line)
|
||||||
|
if match:
|
||||||
|
free_mb += int(match.group(1))
|
||||||
|
continue
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
# silently ignore IO errors (eg. file missing)
|
||||||
|
pass
|
||||||
|
|
||||||
|
total_mb /= 1024
|
||||||
|
free_mb /= 1024
|
||||||
|
self.total_memory_nodes_mb.append(total_mb)
|
||||||
|
attr = {
|
||||||
|
'numa_node': node,
|
||||||
|
'memtotal_mib': total_mb,
|
||||||
|
'memavail_mib': free_mb,
|
||||||
|
'hugepages_configured': 'False',
|
||||||
|
}
|
||||||
|
|
||||||
|
imemory.append(attr)
|
||||||
|
|
||||||
|
return imemory
|
||||||
|
|
||||||
|
def inodes_get_imemory(self):
|
||||||
|
'''Enumerate logical memory topology based on:
|
||||||
|
if CONF.compute_hugepages:
|
||||||
|
self._inode_get_memory_hugepages()
|
||||||
|
else:
|
||||||
|
self._inode_get_memory_nonhugepages()
|
||||||
|
|
||||||
|
:param self
|
||||||
|
:returns list of memory nodes and attributes
|
||||||
|
'''
|
||||||
|
imemory = []
|
||||||
|
|
||||||
|
# if CONF.compute_hugepages:
|
||||||
|
if os.path.isfile("/etc/nova/compute_reserved.conf"):
|
||||||
|
imemory = self._inode_get_memory_hugepages()
|
||||||
|
else:
|
||||||
|
imemory = self._inode_get_memory_nonhugepages()
|
||||||
|
|
||||||
|
LOG.debug("imemory= %s" % imemory)
|
||||||
|
|
||||||
|
return imemory
|
|
@ -0,0 +1,621 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
|
||||||
|
""" inventory pci Utilities and helper functions."""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from inventory.common import k_pci
|
||||||
|
from inventory.common import utils
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
# Look for PCI class 0x0200 and 0x0280 so that we get generic ethernet
|
||||||
|
# controllers and those that may report as "other" network controllers.
|
||||||
|
ETHERNET_PCI_CLASSES = ['ethernet controller', 'network controller']
|
||||||
|
|
||||||
|
# Look for other devices we may want to inventory.
|
||||||
|
KNOWN_PCI_DEVICES = [
|
||||||
|
{"vendor_id": k_pci.NOVA_PCI_ALIAS_QAT_PF_VENDOR,
|
||||||
|
"device_id": k_pci.NOVA_PCI_ALIAS_QAT_DH895XCC_PF_DEVICE,
|
||||||
|
"class_id": k_pci.NOVA_PCI_ALIAS_QAT_CLASS},
|
||||||
|
{"vendor_id": k_pci.NOVA_PCI_ALIAS_QAT_PF_VENDOR,
|
||||||
|
"device_id": k_pci.NOVA_PCI_ALIAS_QAT_C62X_PF_DEVICE,
|
||||||
|
"class_id": k_pci.NOVA_PCI_ALIAS_QAT_CLASS},
|
||||||
|
{"class_id": k_pci.NOVA_PCI_ALIAS_GPU_CLASS}]
|
||||||
|
|
||||||
|
# PCI-SIG 0x06 bridge devices to not inventory.
|
||||||
|
IGNORE_BRIDGE_PCI_CLASSES = ['bridge', 'isa bridge', 'host bridge']
|
||||||
|
|
||||||
|
# PCI-SIG 0x08 generic peripheral devices to not inventory.
|
||||||
|
IGNORE_PERIPHERAL_PCI_CLASSES = ['system peripheral', 'pic', 'dma controller',
|
||||||
|
'iommu', 'rtc']
|
||||||
|
|
||||||
|
# PCI-SIG 0x11 signal processing devices to not inventory.
|
||||||
|
IGNORE_SIGNAL_PROCESSING_PCI_CLASSES = ['performance counters']
|
||||||
|
|
||||||
|
# Blacklist of devices we do not want to inventory, because they are dealt
|
||||||
|
# with separately (ie. Ethernet devices), or do not make sense to expose
|
||||||
|
# to a guest.
|
||||||
|
IGNORE_PCI_CLASSES = ETHERNET_PCI_CLASSES + IGNORE_BRIDGE_PCI_CLASSES + \
|
||||||
|
IGNORE_PERIPHERAL_PCI_CLASSES + IGNORE_SIGNAL_PROCESSING_PCI_CLASSES
|
||||||
|
|
||||||
|
pciaddr = 0
|
||||||
|
pclass = 1
|
||||||
|
pvendor = 2
|
||||||
|
pdevice = 3
|
||||||
|
prevision = 4
|
||||||
|
psvendor = 5
|
||||||
|
psdevice = 6
|
||||||
|
|
||||||
|
VALID_PORT_SPEED = ['10', '100', '1000', '10000', '40000', '100000']
|
||||||
|
|
||||||
|
# Network device flags (from include/uapi/linux/if.h)
|
||||||
|
IFF_UP = 1 << 0
|
||||||
|
IFF_BROADCAST = 1 << 1
|
||||||
|
IFF_DEBUG = 1 << 2
|
||||||
|
IFF_LOOPBACK = 1 << 3
|
||||||
|
IFF_POINTOPOINT = 1 << 4
|
||||||
|
IFF_NOTRAILERS = 1 << 5
|
||||||
|
IFF_RUNNING = 1 << 6
|
||||||
|
IFF_NOARP = 1 << 7
|
||||||
|
IFF_PROMISC = 1 << 8
|
||||||
|
IFF_ALLMULTI = 1 << 9
|
||||||
|
IFF_MASTER = 1 << 10
|
||||||
|
IFF_SLAVE = 1 << 11
|
||||||
|
IFF_MULTICAST = 1 << 12
|
||||||
|
IFF_PORTSEL = 1 << 13
|
||||||
|
IFF_AUTOMEDIA = 1 << 14
|
||||||
|
IFF_DYNAMIC = 1 << 15
|
||||||
|
|
||||||
|
|
||||||
|
class PCI(object):
|
||||||
|
'''Class to encapsulate PCI data for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self, pciaddr, pclass, pvendor, pdevice, prevision,
|
||||||
|
psvendor, psdevice):
|
||||||
|
'''Construct a pci object with the given values.'''
|
||||||
|
|
||||||
|
self.pciaddr = pciaddr
|
||||||
|
self.pclass = pclass
|
||||||
|
self.pvendor = pvendor
|
||||||
|
self.pdevice = pdevice
|
||||||
|
self.prevision = prevision
|
||||||
|
self.psvendor = psvendor
|
||||||
|
self.psdevice = psdevice
|
||||||
|
|
||||||
|
def __eq__(self, rhs):
|
||||||
|
return (self.pvendor == rhs.pvendor and
|
||||||
|
self.pdevice == rhs.pdevice)
|
||||||
|
|
||||||
|
def __ne__(self, rhs):
|
||||||
|
return (self.pvendor != rhs.pvendor or
|
||||||
|
self.pdevice != rhs.pdevice)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s [%s] [%s]" % (self.pciaddr, self.pvendor, self.pdevice)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<PCI '%s'>" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class Port(object):
|
||||||
|
'''Class to encapsulate PCI data for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self, ipci, **kwargs):
|
||||||
|
'''Construct an port object with the given values.'''
|
||||||
|
self.ipci = ipci
|
||||||
|
self.name = kwargs.get('name')
|
||||||
|
self.mac = kwargs.get('mac')
|
||||||
|
self.mtu = kwargs.get('mtu')
|
||||||
|
self.speed = kwargs.get('speed')
|
||||||
|
self.link_mode = kwargs.get('link_mode')
|
||||||
|
self.numa_node = kwargs.get('numa_node')
|
||||||
|
self.dev_id = kwargs.get('dev_id')
|
||||||
|
self.sriov_totalvfs = kwargs.get('sriov_totalvfs')
|
||||||
|
self.sriov_numvfs = kwargs.get('sriov_numvfs')
|
||||||
|
self.sriov_vfs_pci_address = kwargs.get('sriov_vfs_pci_address')
|
||||||
|
self.driver = kwargs.get('driver')
|
||||||
|
self.dpdksupport = kwargs.get('dpdksupport')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s %s: [%s] [%s] [%s], [%s], [%s], [%s], [%s]" % (
|
||||||
|
self.ipci, self.name, self.mac, self.mtu, self.speed,
|
||||||
|
self.link_mode, self.numa_node, self.dev_id, self.dpdksupport)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Port '%s'>" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class PCIDevice(object):
|
||||||
|
'''Class to encapsulate extended PCI data for System Inventory'''
|
||||||
|
|
||||||
|
def __init__(self, pci, **kwargs):
|
||||||
|
'''Construct a PciDevice object with the given values.'''
|
||||||
|
self.pci = pci
|
||||||
|
self.name = kwargs.get('name')
|
||||||
|
self.pclass_id = kwargs.get('pclass_id')
|
||||||
|
self.pvendor_id = kwargs.get('pvendor_id')
|
||||||
|
self.pdevice_id = kwargs.get('pdevice_id')
|
||||||
|
self.numa_node = kwargs.get('numa_node')
|
||||||
|
self.sriov_totalvfs = kwargs.get('sriov_totalvfs')
|
||||||
|
self.sriov_numvfs = kwargs.get('sriov_numvfs')
|
||||||
|
self.sriov_vfs_pci_address = kwargs.get('sriov_vfs_pci_address')
|
||||||
|
self.driver = kwargs.get('driver')
|
||||||
|
self.enabled = kwargs.get('enabled')
|
||||||
|
self.extra_info = kwargs.get('extra_info')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s %s: [%s]" % (
|
||||||
|
self.pci, self.numa_node, self.driver)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<PCIDevice '%s'>" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
class PCIOperator(object):
|
||||||
|
'''Class to encapsulate PCI operations for System Inventory'''
|
||||||
|
|
||||||
|
def format_lspci_output(self, device):
|
||||||
|
# hack for now
|
||||||
|
if device[prevision].strip() == device[pvendor].strip():
|
||||||
|
# no revision info
|
||||||
|
device.append(device[psvendor])
|
||||||
|
device[psvendor] = device[prevision]
|
||||||
|
device[prevision] = "0"
|
||||||
|
elif len(device) <= 6: # one less entry, no revision
|
||||||
|
LOG.debug("update psdevice length=%s" % len(device))
|
||||||
|
device.append(device[psvendor])
|
||||||
|
return device
|
||||||
|
|
||||||
|
def get_pci_numa_node(self, pciaddr):
|
||||||
|
fnuma_node = '/sys/bus/pci/devices/' + pciaddr + '/numa_node'
|
||||||
|
try:
|
||||||
|
with open(fnuma_node, 'r') as f:
|
||||||
|
numa_node = f.readline().strip()
|
||||||
|
LOG.debug("ATTR numa_node: %s " % numa_node)
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR numa_node unknown for: %s " % pciaddr)
|
||||||
|
numa_node = None
|
||||||
|
return numa_node
|
||||||
|
|
||||||
|
def get_pci_sriov_totalvfs(self, pciaddr):
|
||||||
|
fsriov_totalvfs = '/sys/bus/pci/devices/' + pciaddr + '/sriov_totalvfs'
|
||||||
|
try:
|
||||||
|
with open(fsriov_totalvfs, 'r') as f:
|
||||||
|
sriov_totalvfs = f.readline()
|
||||||
|
LOG.debug("ATTR sriov_totalvfs: %s " % sriov_totalvfs)
|
||||||
|
f.close()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR sriov_totalvfs unknown for: %s " % pciaddr)
|
||||||
|
sriov_totalvfs = None
|
||||||
|
pass
|
||||||
|
return sriov_totalvfs
|
||||||
|
|
||||||
|
def get_pci_sriov_numvfs(self, pciaddr):
|
||||||
|
fsriov_numvfs = '/sys/bus/pci/devices/' + pciaddr + '/sriov_numvfs'
|
||||||
|
try:
|
||||||
|
with open(fsriov_numvfs, 'r') as f:
|
||||||
|
sriov_numvfs = f.readline()
|
||||||
|
LOG.debug("ATTR sriov_numvfs: %s " % sriov_numvfs)
|
||||||
|
f.close()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR sriov_numvfs unknown for: %s " % pciaddr)
|
||||||
|
sriov_numvfs = 0
|
||||||
|
pass
|
||||||
|
LOG.debug("sriov_numvfs: %s" % sriov_numvfs)
|
||||||
|
return sriov_numvfs
|
||||||
|
|
||||||
|
def get_pci_sriov_vfs_pci_address(self, pciaddr, sriov_numvfs):
|
||||||
|
dirpcidev = '/sys/bus/pci/devices/' + pciaddr
|
||||||
|
sriov_vfs_pci_address = []
|
||||||
|
i = 0
|
||||||
|
while i < int(sriov_numvfs):
|
||||||
|
lvf = dirpcidev + '/virtfn' + str(i)
|
||||||
|
try:
|
||||||
|
sriov_vfs_pci_address.append(
|
||||||
|
os.path.basename(os.readlink(lvf)))
|
||||||
|
except Exception:
|
||||||
|
LOG.warning("virtfn link %s non-existent (sriov_numvfs=%s)"
|
||||||
|
% (lvf, sriov_numvfs))
|
||||||
|
pass
|
||||||
|
i += 1
|
||||||
|
LOG.debug("sriov_vfs_pci_address: %s" % sriov_vfs_pci_address)
|
||||||
|
return sriov_vfs_pci_address
|
||||||
|
|
||||||
|
def get_pci_driver_name(self, pciaddr):
|
||||||
|
ddriver = '/sys/bus/pci/devices/' + pciaddr + '/driver/module/drivers'
|
||||||
|
try:
|
||||||
|
drivers = [
|
||||||
|
os.path.basename(os.readlink(ddriver + '/' + d))
|
||||||
|
for d in os.listdir(ddriver)]
|
||||||
|
driver = str(','.join(str(d) for d in drivers))
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR driver unknown for: %s " % pciaddr)
|
||||||
|
driver = None
|
||||||
|
pass
|
||||||
|
LOG.debug("driver: %s" % driver)
|
||||||
|
return driver
|
||||||
|
|
||||||
|
def pci_devices_get(self):
|
||||||
|
|
||||||
|
p = subprocess.Popen(["lspci", "-Dm"], stdout=subprocess.PIPE)
|
||||||
|
|
||||||
|
pci_devices = []
|
||||||
|
for line in p.stdout:
|
||||||
|
pci_device = shlex.split(line.strip())
|
||||||
|
pci_device = self.format_lspci_output(pci_device)
|
||||||
|
|
||||||
|
if any(x in pci_device[pclass].lower() for x in
|
||||||
|
IGNORE_PCI_CLASSES):
|
||||||
|
continue
|
||||||
|
|
||||||
|
dirpcidev = '/sys/bus/pci/devices/'
|
||||||
|
physfn = dirpcidev + pci_device[pciaddr] + '/physfn'
|
||||||
|
if not os.path.isdir(physfn):
|
||||||
|
# Do not report VFs
|
||||||
|
pci_devices.append(PCI(pci_device[pciaddr],
|
||||||
|
pci_device[pclass],
|
||||||
|
pci_device[pvendor],
|
||||||
|
pci_device[pdevice],
|
||||||
|
pci_device[prevision],
|
||||||
|
pci_device[psvendor],
|
||||||
|
pci_device[psdevice]))
|
||||||
|
|
||||||
|
p.wait()
|
||||||
|
|
||||||
|
return pci_devices
|
||||||
|
|
||||||
|
def inics_get(self):
|
||||||
|
|
||||||
|
p = subprocess.Popen(["lspci", "-Dm"], stdout=subprocess.PIPE)
|
||||||
|
|
||||||
|
pci_inics = []
|
||||||
|
for line in p.stdout:
|
||||||
|
inic = shlex.split(line.strip())
|
||||||
|
if any(x in inic[pclass].lower() for x in ETHERNET_PCI_CLASSES):
|
||||||
|
# hack for now
|
||||||
|
if inic[prevision].strip() == inic[pvendor].strip():
|
||||||
|
# no revision info
|
||||||
|
inic.append(inic[psvendor])
|
||||||
|
inic[psvendor] = inic[prevision]
|
||||||
|
inic[prevision] = "0"
|
||||||
|
elif len(inic) <= 6: # one less entry, no revision
|
||||||
|
LOG.debug("update psdevice length=%s" % len(inic))
|
||||||
|
inic.append(inic[psvendor])
|
||||||
|
|
||||||
|
dirpcidev = '/sys/bus/pci/devices/'
|
||||||
|
physfn = dirpcidev + inic[pciaddr] + '/physfn'
|
||||||
|
if os.path.isdir(physfn):
|
||||||
|
# Do not report VFs
|
||||||
|
continue
|
||||||
|
pci_inics.append(PCI(inic[pciaddr], inic[pclass],
|
||||||
|
inic[pvendor], inic[pdevice],
|
||||||
|
inic[prevision], inic[psvendor],
|
||||||
|
inic[psdevice]))
|
||||||
|
|
||||||
|
p.wait()
|
||||||
|
|
||||||
|
return pci_inics
|
||||||
|
|
||||||
|
def pci_get_enabled_attr(self, class_id, vendor_id, product_id):
|
||||||
|
for known_device in KNOWN_PCI_DEVICES:
|
||||||
|
if (class_id == known_device.get("class_id", None) or
|
||||||
|
(vendor_id == known_device.get("vendor_id", None) and
|
||||||
|
product_id == known_device.get("device_id", None))):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def pci_get_device_attrs(self, pciaddr):
|
||||||
|
"""For this pciaddr, build a list of device attributes """
|
||||||
|
pci_attrs_array = []
|
||||||
|
|
||||||
|
dirpcidev = '/sys/bus/pci/devices/'
|
||||||
|
pciaddrs = os.listdir(dirpcidev)
|
||||||
|
|
||||||
|
for a in pciaddrs:
|
||||||
|
if ((a == pciaddr) or (a == ("0000:" + pciaddr))):
|
||||||
|
LOG.debug("Found device pci bus: %s " % a)
|
||||||
|
|
||||||
|
dirpcideva = dirpcidev + a
|
||||||
|
|
||||||
|
numa_node = self.get_pci_numa_node(a)
|
||||||
|
sriov_totalvfs = self.get_pci_sriov_totalvfs(a)
|
||||||
|
sriov_numvfs = self.get_pci_sriov_numvfs(a)
|
||||||
|
sriov_vfs_pci_address = \
|
||||||
|
self.get_pci_sriov_vfs_pci_address(a, sriov_numvfs)
|
||||||
|
driver = self.get_pci_driver_name(a)
|
||||||
|
|
||||||
|
fclass = dirpcideva + '/class'
|
||||||
|
fvendor = dirpcideva + '/vendor'
|
||||||
|
fdevice = dirpcideva + '/device'
|
||||||
|
try:
|
||||||
|
with open(fvendor, 'r') as f:
|
||||||
|
pvendor_id = f.readline().strip('0x').strip()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR vendor unknown for: %s " % a)
|
||||||
|
pvendor_id = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(fdevice, 'r') as f:
|
||||||
|
pdevice_id = f.readline().replace('0x', '').strip()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR device unknown for: %s " % a)
|
||||||
|
pdevice_id = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(fclass, 'r') as f:
|
||||||
|
pclass_id = f.readline().replace('0x', '').strip()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR class unknown for: %s " % a)
|
||||||
|
pclass_id = None
|
||||||
|
|
||||||
|
name = "pci_" + a.replace(':', '_').replace('.', '_')
|
||||||
|
|
||||||
|
attrs = {
|
||||||
|
"name": name,
|
||||||
|
"pci_address": a,
|
||||||
|
"pclass_id": pclass_id,
|
||||||
|
"pvendor_id": pvendor_id,
|
||||||
|
"pdevice_id": pdevice_id,
|
||||||
|
"numa_node": numa_node,
|
||||||
|
"sriov_totalvfs": sriov_totalvfs,
|
||||||
|
"sriov_numvfs": sriov_numvfs,
|
||||||
|
"sriov_vfs_pci_address":
|
||||||
|
','.join(str(x) for x in sriov_vfs_pci_address),
|
||||||
|
"driver": driver,
|
||||||
|
"enabled": self.pci_get_enabled_attr(
|
||||||
|
pclass_id, pvendor_id, pdevice_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
pci_attrs_array.append(attrs)
|
||||||
|
|
||||||
|
return pci_attrs_array
|
||||||
|
|
||||||
|
def get_pci_net_directory(self, pciaddr):
|
||||||
|
device_directory = '/sys/bus/pci/devices/' + pciaddr
|
||||||
|
# Look for the standard device 'net' directory
|
||||||
|
net_directory = device_directory + '/net/'
|
||||||
|
if os.path.exists(net_directory):
|
||||||
|
return net_directory
|
||||||
|
# Otherwise check whether this is a virtio based device
|
||||||
|
net_pattern = device_directory + '/virtio*/net/'
|
||||||
|
results = glob.glob(net_pattern)
|
||||||
|
if not results:
|
||||||
|
return None
|
||||||
|
if len(results) > 1:
|
||||||
|
LOG.warning("PCI device {} has multiple virtio "
|
||||||
|
"sub-directories".format(pciaddr))
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
def _read_flags(self, fflags):
|
||||||
|
try:
|
||||||
|
with open(fflags, 'r') as f:
|
||||||
|
hex_str = f.readline().rstrip()
|
||||||
|
flags = int(hex_str, 16)
|
||||||
|
except Exception:
|
||||||
|
flags = None
|
||||||
|
return flags
|
||||||
|
|
||||||
|
def _get_netdev_flags(self, dirpcinet, pci):
|
||||||
|
fflags = dirpcinet + pci + '/flags'
|
||||||
|
return self._read_flags(fflags)
|
||||||
|
|
||||||
|
def pci_get_net_flags(self, name):
|
||||||
|
fflags = '/sys/class/net/' + name + '/flags'
|
||||||
|
return self._read_flags(fflags)
|
||||||
|
|
||||||
|
def pci_get_net_names(self):
|
||||||
|
'''build a list of network device names.'''
|
||||||
|
names = []
|
||||||
|
for name in os.listdir('/sys/class/net/'):
|
||||||
|
if os.path.isdir('/sys/class/net/' + name):
|
||||||
|
names.append(name)
|
||||||
|
return names
|
||||||
|
|
||||||
|
def pci_get_net_attrs(self, pciaddr):
|
||||||
|
"""For this pciaddr, build a list of network attributes per port"""
|
||||||
|
pci_attrs_array = []
|
||||||
|
|
||||||
|
dirpcidev = '/sys/bus/pci/devices/'
|
||||||
|
pciaddrs = os.listdir(dirpcidev)
|
||||||
|
|
||||||
|
for a in pciaddrs:
|
||||||
|
if ((a == pciaddr) or (a == ("0000:" + pciaddr))):
|
||||||
|
# Look inside net expect to find address,speed,mtu etc. info
|
||||||
|
# There may be more than 1 net device for this NIC.
|
||||||
|
LOG.debug("Found NIC pci bus: %s " % a)
|
||||||
|
|
||||||
|
dirpcideva = dirpcidev + a
|
||||||
|
|
||||||
|
numa_node = self.get_pci_numa_node(a)
|
||||||
|
sriov_totalvfs = self.get_pci_sriov_totalvfs(a)
|
||||||
|
sriov_numvfs = self.get_pci_sriov_numvfs(a)
|
||||||
|
sriov_vfs_pci_address = \
|
||||||
|
self.get_pci_sriov_vfs_pci_address(a, sriov_numvfs)
|
||||||
|
driver = self.get_pci_driver_name(a)
|
||||||
|
|
||||||
|
# Determine DPDK support
|
||||||
|
dpdksupport = False
|
||||||
|
fvendor = dirpcideva + '/vendor'
|
||||||
|
fdevice = dirpcideva + '/device'
|
||||||
|
try:
|
||||||
|
with open(fvendor, 'r') as f:
|
||||||
|
vendor = f.readline().strip()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR vendor unknown for: %s " % a)
|
||||||
|
vendor = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(fdevice, 'r') as f:
|
||||||
|
device = f.readline().strip()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR device unknown for: %s " % a)
|
||||||
|
device = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(os.devnull, "w") as fnull:
|
||||||
|
subprocess.check_call(
|
||||||
|
["query_pci_id", "-v " + str(vendor),
|
||||||
|
"-d " + str(device)],
|
||||||
|
stdout=fnull, stderr=fnull)
|
||||||
|
dpdksupport = True
|
||||||
|
LOG.debug("DPDK does support NIC "
|
||||||
|
"(vendor: %s device: %s)",
|
||||||
|
vendor, device)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
dpdksupport = False
|
||||||
|
if e.returncode == 1:
|
||||||
|
# NIC is not supprted
|
||||||
|
LOG.debug("DPDK does not support NIC "
|
||||||
|
"(vendor: %s device: %s)",
|
||||||
|
vendor, device)
|
||||||
|
else:
|
||||||
|
# command failed, default to DPDK support to False
|
||||||
|
LOG.info("Could not determine DPDK support for "
|
||||||
|
"NIC (vendor %s device: %s), defaulting "
|
||||||
|
"to False", vendor, device)
|
||||||
|
|
||||||
|
# determine the net directory for this device
|
||||||
|
dirpcinet = self.get_pci_net_directory(a)
|
||||||
|
if dirpcinet is None:
|
||||||
|
LOG.warning("no /net for PCI device: %s " % a)
|
||||||
|
continue # go to next PCI device
|
||||||
|
|
||||||
|
# determine which netdevs are associated to this device
|
||||||
|
netdevs = os.listdir(dirpcinet)
|
||||||
|
for n in netdevs:
|
||||||
|
mac = None
|
||||||
|
fmac = dirpcinet + n + '/' + "address"
|
||||||
|
fmaster = dirpcinet + n + '/' + "master"
|
||||||
|
# if a port is a member of a bond the port MAC address
|
||||||
|
# must be retrieved from /proc/net/bonding/<bond_name>
|
||||||
|
if os.path.exists(fmaster):
|
||||||
|
dirmaster = os.path.realpath(fmaster)
|
||||||
|
master_name = os.path.basename(dirmaster)
|
||||||
|
procnetbonding = '/proc/net/bonding/' + master_name
|
||||||
|
found_interface = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(procnetbonding, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if 'Slave Interface: ' + n in line:
|
||||||
|
found_interface = True
|
||||||
|
if (found_interface and
|
||||||
|
'Permanent HW addr:' in line):
|
||||||
|
mac = line.split(': ')[1].rstrip()
|
||||||
|
mac = utils.validate_and_normalize_mac(
|
||||||
|
mac)
|
||||||
|
break
|
||||||
|
if not mac:
|
||||||
|
LOG.info("ATTR mac could not be determined"
|
||||||
|
" for slave interface %s" % n)
|
||||||
|
except Exception:
|
||||||
|
LOG.info("ATTR mac could not be determined, "
|
||||||
|
"could not open %s" % procnetbonding)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
with open(fmac, 'r') as f:
|
||||||
|
mac = f.readline().rstrip()
|
||||||
|
mac = utils.validate_and_normalize_mac(mac)
|
||||||
|
except Exception:
|
||||||
|
LOG.info("ATTR mac unknown for: %s " % n)
|
||||||
|
|
||||||
|
fmtu = dirpcinet + n + '/' + "mtu"
|
||||||
|
try:
|
||||||
|
with open(fmtu, 'r') as f:
|
||||||
|
mtu = f.readline().rstrip()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR mtu unknown for: %s " % n)
|
||||||
|
mtu = None
|
||||||
|
|
||||||
|
# Check the administrative state before reading the speed
|
||||||
|
flags = self._get_netdev_flags(dirpcinet, n)
|
||||||
|
|
||||||
|
# If administrative state is down, bring it up momentarily
|
||||||
|
if not(flags & IFF_UP):
|
||||||
|
LOG.warning("Enabling device %s to query link speed" %
|
||||||
|
n)
|
||||||
|
cmd = 'ip link set dev %s up' % n
|
||||||
|
subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||||
|
shell=True)
|
||||||
|
# Read the speed
|
||||||
|
fspeed = dirpcinet + n + '/' + "speed"
|
||||||
|
try:
|
||||||
|
with open(fspeed, 'r') as f:
|
||||||
|
speed = f.readline().rstrip()
|
||||||
|
if speed not in VALID_PORT_SPEED:
|
||||||
|
LOG.error("Invalid port speed = %s for %s " %
|
||||||
|
(speed, n))
|
||||||
|
speed = None
|
||||||
|
except Exception:
|
||||||
|
LOG.warning("ATTR speed unknown for: %s "
|
||||||
|
"(flags: %s)" % (n, hex(flags)))
|
||||||
|
speed = None
|
||||||
|
# If the administrative state was down, take it back down
|
||||||
|
if not(flags & IFF_UP):
|
||||||
|
LOG.warning("Disabling device %s after querying "
|
||||||
|
"link speed" % n)
|
||||||
|
cmd = 'ip link set dev %s down' % n
|
||||||
|
subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||||
|
shell=True)
|
||||||
|
|
||||||
|
flink_mode = dirpcinet + n + '/' + "link_mode"
|
||||||
|
try:
|
||||||
|
with open(flink_mode, 'r') as f:
|
||||||
|
link_mode = f.readline().rstrip()
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR link_mode unknown for: %s " % n)
|
||||||
|
link_mode = None
|
||||||
|
|
||||||
|
fdevport = dirpcinet + n + '/' + "dev_port"
|
||||||
|
try:
|
||||||
|
with open(fdevport, 'r') as f:
|
||||||
|
dev_port = int(f.readline().rstrip(), 0)
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR dev_port unknown for: %s " % n)
|
||||||
|
# Kernel versions older than 3.15 used dev_id
|
||||||
|
# (incorrectly) to identify the network devices,
|
||||||
|
# therefore support the fallback if dev_port is not
|
||||||
|
# available
|
||||||
|
try:
|
||||||
|
fdevid = dirpcinet + n + '/' + "dev_id"
|
||||||
|
with open(fdevid, 'r') as f:
|
||||||
|
dev_port = int(f.readline().rstrip(), 0)
|
||||||
|
except Exception:
|
||||||
|
LOG.debug("ATTR dev_id unknown for: %s " % n)
|
||||||
|
dev_port = 0
|
||||||
|
|
||||||
|
attrs = {
|
||||||
|
"name": n,
|
||||||
|
"numa_node": numa_node,
|
||||||
|
"sriov_totalvfs": sriov_totalvfs,
|
||||||
|
"sriov_numvfs": sriov_numvfs,
|
||||||
|
"sriov_vfs_pci_address":
|
||||||
|
','.join(str(x) for x in sriov_vfs_pci_address),
|
||||||
|
"driver": driver,
|
||||||
|
"pci_address": a,
|
||||||
|
"mac": mac,
|
||||||
|
"mtu": mtu,
|
||||||
|
"speed": speed,
|
||||||
|
"link_mode": link_mode,
|
||||||
|
"dev_id": dev_port,
|
||||||
|
"dpdksupport": dpdksupport
|
||||||
|
}
|
||||||
|
|
||||||
|
pci_attrs_array.append(attrs)
|
||||||
|
|
||||||
|
return pci_attrs_array
|
|
@ -0,0 +1,161 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Client side of the agent RPC API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
import oslo_messaging as messaging
|
||||||
|
|
||||||
|
from inventory.common import rpc
|
||||||
|
from inventory.objects import base as objects_base
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
MANAGER_TOPIC = 'inventory.agent_manager'
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAPI(object):
|
||||||
|
"""Client side of the agent RPC API.
|
||||||
|
|
||||||
|
API version history:
|
||||||
|
|
||||||
|
1.0 - Initial version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RPC_API_VERSION = '1.0'
|
||||||
|
|
||||||
|
def __init__(self, topic=None):
|
||||||
|
|
||||||
|
super(AgentAPI, self).__init__()
|
||||||
|
self.topic = topic
|
||||||
|
if self.topic is None:
|
||||||
|
self.topic = MANAGER_TOPIC
|
||||||
|
target = messaging.Target(topic=self.topic,
|
||||||
|
version='1.0')
|
||||||
|
serializer = objects_base.InventoryObjectSerializer()
|
||||||
|
version_cap = self.RPC_API_VERSION
|
||||||
|
self.client = rpc.get_client(target,
|
||||||
|
version_cap=version_cap,
|
||||||
|
serializer=serializer)
|
||||||
|
|
||||||
|
def host_inventory(self, context, values, topic=None):
|
||||||
|
"""Synchronously, have a agent collect inventory for this host.
|
||||||
|
|
||||||
|
Collect ihost inventory and report to conductor.
|
||||||
|
|
||||||
|
:param context: request context.
|
||||||
|
:param values: dictionary with initial values for new host object
|
||||||
|
:returns: created ihost object, including all fields.
|
||||||
|
"""
|
||||||
|
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
|
||||||
|
return cctxt.call(context,
|
||||||
|
'host_inventory',
|
||||||
|
values=values)
|
||||||
|
|
||||||
|
def configure_ttys_dcd(self, context, uuid, ttys_dcd, topic=None):
|
||||||
|
"""Asynchronously, have the agent configure the getty on the serial
|
||||||
|
console.
|
||||||
|
|
||||||
|
:param context: request context.
|
||||||
|
:param uuid: the host uuid
|
||||||
|
:param ttys_dcd: the flag to enable/disable dcd
|
||||||
|
:returns: none ... uses asynchronous cast().
|
||||||
|
"""
|
||||||
|
# fanout / broadcast message to all inventory agents
|
||||||
|
LOG.debug("AgentApi.configure_ttys_dcd: fanout_cast: sending "
|
||||||
|
"dcd update to agent: (%s) (%s" % (uuid, ttys_dcd))
|
||||||
|
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0',
|
||||||
|
fanout=True)
|
||||||
|
retval = cctxt.cast(context,
|
||||||
|
'configure_ttys_dcd',
|
||||||
|
uuid=uuid,
|
||||||
|
ttys_dcd=ttys_dcd)
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def execute_command(self, context, host_uuid, command, topic=None):
|
||||||
|
"""Asynchronously, have the agent execute a command
|
||||||
|
|
||||||
|
:param context: request context.
|
||||||
|
:param host_uuid: the host uuid
|
||||||
|
:param command: the command to execute
|
||||||
|
:returns: none ... uses asynchronous cast().
|
||||||
|
"""
|
||||||
|
# fanout / broadcast message to all inventory agents
|
||||||
|
LOG.debug("AgentApi.update_cpu_config: fanout_cast: sending "
|
||||||
|
"host uuid: (%s) " % host_uuid)
|
||||||
|
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0',
|
||||||
|
fanout=True)
|
||||||
|
retval = cctxt.cast(context,
|
||||||
|
'execute_command',
|
||||||
|
host_uuid=host_uuid,
|
||||||
|
command=command)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def agent_update(self, context, host_uuid, force_updates,
|
||||||
|
cinder_device=None,
|
||||||
|
topic=None):
|
||||||
|
"""
|
||||||
|
Asynchronously, have the agent update partitions, ipv and ilvg state
|
||||||
|
|
||||||
|
:param context: request context
|
||||||
|
:param host_uuid: the host uuid
|
||||||
|
:param force_updates: list of inventory objects to update
|
||||||
|
:param cinder_device: device by path of cinder volumes
|
||||||
|
:return: none ... uses asynchronous cast().
|
||||||
|
"""
|
||||||
|
|
||||||
|
# fanout / broadcast message to all inventory agents
|
||||||
|
LOG.info("AgentApi.agent_update: fanout_cast: sending "
|
||||||
|
"update request to agent for: (%s)" %
|
||||||
|
(', '.join(force_updates)))
|
||||||
|
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0',
|
||||||
|
fanout=True)
|
||||||
|
retval = cctxt.cast(context,
|
||||||
|
'agent_audit',
|
||||||
|
host_uuid=host_uuid,
|
||||||
|
force_updates=force_updates,
|
||||||
|
cinder_device=cinder_device)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def disk_format_gpt(self, context, host_uuid, idisk_dict,
|
||||||
|
is_cinder_device, topic=None):
|
||||||
|
"""Asynchronously, GPT format a disk.
|
||||||
|
|
||||||
|
:param context: an admin context
|
||||||
|
:param host_uuid: ihost uuid unique id
|
||||||
|
:param idisk_dict: values for disk object
|
||||||
|
:param is_cinder_device: bool value tells if the idisk is for cinder
|
||||||
|
:returns: pass or fail
|
||||||
|
"""
|
||||||
|
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0',
|
||||||
|
fanout=True)
|
||||||
|
|
||||||
|
return cctxt.cast(context,
|
||||||
|
'disk_format_gpt',
|
||||||
|
host_uuid=host_uuid,
|
||||||
|
idisk_dict=idisk_dict,
|
||||||
|
is_cinder_device=is_cinder_device)
|
|
@ -0,0 +1,90 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_service import service
|
||||||
|
from oslo_service import wsgi
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from inventory.api import config
|
||||||
|
from inventory.api import middleware
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import policy
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
_launcher = None
|
||||||
|
_launcher_pxe = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_pecan_config():
|
||||||
|
# Set up the pecan configuration
|
||||||
|
filename = config.__file__.replace('.pyc', '.py')
|
||||||
|
return pecan.configuration.conf_from_file(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_app(config=None):
|
||||||
|
policy.init_enforcer()
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
config = get_pecan_config()
|
||||||
|
|
||||||
|
pecan.configuration.set_config(dict(config), overwrite=True)
|
||||||
|
app_conf = dict(config.app)
|
||||||
|
|
||||||
|
app = pecan.make_app(
|
||||||
|
app_conf.pop('root'),
|
||||||
|
debug=CONF.debug,
|
||||||
|
logging=getattr(config, 'logging', {}),
|
||||||
|
force_canonical=getattr(config.app, 'force_canonical', True),
|
||||||
|
guess_content_type_from_ext=False,
|
||||||
|
wrap_app=middleware.ParsableErrorMiddleware,
|
||||||
|
**app_conf
|
||||||
|
)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def load_paste_app(app_name=None):
|
||||||
|
"""Loads a WSGI app from a paste config file."""
|
||||||
|
if app_name is None:
|
||||||
|
app_name = cfg.CONF.prog
|
||||||
|
|
||||||
|
loader = wsgi.Loader(cfg.CONF)
|
||||||
|
app = loader.load_app(app_name)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def app_factory(global_config, **local_conf):
|
||||||
|
return setup_app()
|
||||||
|
|
||||||
|
|
||||||
|
def serve(api_service, conf, workers=1):
|
||||||
|
global _launcher
|
||||||
|
|
||||||
|
if _launcher:
|
||||||
|
raise RuntimeError(_('serve() _launcher can only be called once'))
|
||||||
|
|
||||||
|
_launcher = service.launch(conf, api_service, workers=workers)
|
||||||
|
|
||||||
|
|
||||||
|
def serve_pxe(api_service, conf, workers=1):
|
||||||
|
global _launcher_pxe
|
||||||
|
|
||||||
|
if _launcher_pxe:
|
||||||
|
raise RuntimeError(_('serve() _launcher_pxe can only be called once'))
|
||||||
|
|
||||||
|
_launcher_pxe = service.launch(conf, api_service, workers=workers)
|
||||||
|
|
||||||
|
|
||||||
|
def wait():
|
||||||
|
_launcher.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def wait_pxe():
|
||||||
|
_launcher_pxe.wait()
|
|
@ -0,0 +1,73 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from inventory.api import hooks
|
||||||
|
from inventory.common import config
|
||||||
|
from inventory import objects
|
||||||
|
from keystoneauth1 import loading as ks_loading
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import pbr.version
|
||||||
|
import sys
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
sysinv_group = cfg.OptGroup(
|
||||||
|
'sysinv',
|
||||||
|
title='Sysinv Options',
|
||||||
|
help="Configuration options for the platform service")
|
||||||
|
|
||||||
|
sysinv_opts = [
|
||||||
|
cfg.StrOpt('catalog_info',
|
||||||
|
default='platform:sysinv:internalURL',
|
||||||
|
help="Service catalog Look up info."),
|
||||||
|
cfg.StrOpt('os_region_name',
|
||||||
|
default='RegionOne',
|
||||||
|
help="Region name of this node. It is used for catalog lookup"),
|
||||||
|
]
|
||||||
|
|
||||||
|
version_info = pbr.version.VersionInfo('inventory')
|
||||||
|
|
||||||
|
# Pecan Application Configurations
|
||||||
|
app = {
|
||||||
|
'root': 'inventory.api.controllers.root.RootController',
|
||||||
|
'modules': ['inventory.api'],
|
||||||
|
'hooks': [
|
||||||
|
hooks.DBHook(),
|
||||||
|
hooks.ContextHook(),
|
||||||
|
hooks.RPCHook(),
|
||||||
|
hooks.SystemConfigHook(),
|
||||||
|
],
|
||||||
|
'acl_public_routes': [
|
||||||
|
'/',
|
||||||
|
'/v1',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def init(args, **kwargs):
|
||||||
|
cfg.CONF.register_group(sysinv_group)
|
||||||
|
cfg.CONF.register_opts(sysinv_opts, group=sysinv_group)
|
||||||
|
ks_loading.register_session_conf_options(cfg.CONF,
|
||||||
|
sysinv_group.name)
|
||||||
|
logging.register_options(cfg.CONF)
|
||||||
|
|
||||||
|
cfg.CONF(args=args, project='inventory',
|
||||||
|
version='%%(prog)s %s' % version_info.release_string(),
|
||||||
|
**kwargs)
|
||||||
|
objects.register_all()
|
||||||
|
config.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Sets up the logging options for a log with supplied name."""
|
||||||
|
logging.setup(cfg.CONF, "inventory")
|
||||||
|
LOG.debug("Logging enabled!")
|
||||||
|
LOG.debug("%(prog)s version %(version)s",
|
||||||
|
{'prog': sys.argv[0],
|
||||||
|
'version': version_info.release_string()})
|
||||||
|
LOG.debug("command line: %s", " ".join(sys.argv))
|
|
@ -0,0 +1,115 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers import v1
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
|
||||||
|
ID_VERSION = 'v1'
|
||||||
|
|
||||||
|
|
||||||
|
def expose(*args, **kwargs):
|
||||||
|
"""Ensure that only JSON, and not XML, is supported."""
|
||||||
|
if 'rest_content_types' not in kwargs:
|
||||||
|
kwargs['rest_content_types'] = ('json',)
|
||||||
|
return wsme_pecan.wsexpose(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Version(base.APIBase):
|
||||||
|
"""An API version representation.
|
||||||
|
|
||||||
|
This class represents an API version, including the minimum and
|
||||||
|
maximum minor versions that are supported within the major version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = wtypes.text
|
||||||
|
"""The ID of the (major) version, also acts as the release number"""
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"""A Link that point to a specific version of the API"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert(cls, vid):
|
||||||
|
version = Version()
|
||||||
|
version.id = vid
|
||||||
|
version.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
vid, '', bookmark=True)]
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
class Root(base.APIBase):
|
||||||
|
|
||||||
|
name = wtypes.text
|
||||||
|
"""The name of the API"""
|
||||||
|
|
||||||
|
description = wtypes.text
|
||||||
|
"""Some information about this API"""
|
||||||
|
|
||||||
|
versions = [Version]
|
||||||
|
"""Links to all the versions available in this API"""
|
||||||
|
|
||||||
|
default_version = Version
|
||||||
|
"""A link to the default version of the API"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert():
|
||||||
|
root = Root()
|
||||||
|
root.name = "Inventory API"
|
||||||
|
root.description = ("Inventory is an OpenStack project which "
|
||||||
|
"provides REST API services for "
|
||||||
|
"system configuration.")
|
||||||
|
root.default_version = Version.convert(ID_VERSION)
|
||||||
|
root.versions = [root.default_version]
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
class RootController(rest.RestController):
|
||||||
|
|
||||||
|
_versions = [ID_VERSION]
|
||||||
|
"""All supported API versions"""
|
||||||
|
|
||||||
|
_default_version = ID_VERSION
|
||||||
|
"""The default API version"""
|
||||||
|
|
||||||
|
v1 = v1.Controller()
|
||||||
|
|
||||||
|
@expose(Root)
|
||||||
|
def get(self):
|
||||||
|
# NOTE: The reason why convert() it's being called for every
|
||||||
|
# request is because we need to get the host url from
|
||||||
|
# the request object to make the links.
|
||||||
|
return Root.convert()
|
||||||
|
|
||||||
|
@pecan.expose()
|
||||||
|
def _route(self, args, request=None):
|
||||||
|
"""Overrides the default routing behavior.
|
||||||
|
|
||||||
|
It redirects the request to the default version of the Inventory API
|
||||||
|
if the version number is not specified in the url.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if args[0] and args[0] not in self._versions:
|
||||||
|
args = [self._default_version] + args
|
||||||
|
return super(RootController, self)._route(args, request)
|
|
@ -0,0 +1,198 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import cpu
|
||||||
|
from inventory.api.controllers.v1 import ethernet_port
|
||||||
|
from inventory.api.controllers.v1 import host
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import lldp_agent
|
||||||
|
from inventory.api.controllers.v1 import lldp_neighbour
|
||||||
|
from inventory.api.controllers.v1 import memory
|
||||||
|
from inventory.api.controllers.v1 import node
|
||||||
|
from inventory.api.controllers.v1 import pci_device
|
||||||
|
from inventory.api.controllers.v1 import port
|
||||||
|
from inventory.api.controllers.v1 import sensor
|
||||||
|
from inventory.api.controllers.v1 import sensorgroup
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import system
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
|
||||||
|
class MediaType(base.APIBase):
|
||||||
|
"""A media type representation."""
|
||||||
|
|
||||||
|
base = wtypes.text
|
||||||
|
type = wtypes.text
|
||||||
|
|
||||||
|
def __init__(self, base, type):
|
||||||
|
self.base = base
|
||||||
|
self.type = type
|
||||||
|
|
||||||
|
|
||||||
|
class V1(base.APIBase):
|
||||||
|
"""The representation of the version 1 of the API."""
|
||||||
|
|
||||||
|
id = wtypes.text
|
||||||
|
"The ID of the version, also acts as the release number"
|
||||||
|
|
||||||
|
media_types = [MediaType]
|
||||||
|
"An array of supported media types for this version"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Links that point to a specific URL for this version and documentation"
|
||||||
|
|
||||||
|
systems = [link.Link]
|
||||||
|
"Links to the system resource"
|
||||||
|
|
||||||
|
hosts = [link.Link]
|
||||||
|
"Links to the host resource"
|
||||||
|
|
||||||
|
lldp_agents = [link.Link]
|
||||||
|
"Links to the lldp agents resource"
|
||||||
|
|
||||||
|
lldp_neighbours = [link.Link]
|
||||||
|
"Links to the lldp neighbours resource"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert(self):
|
||||||
|
v1 = V1()
|
||||||
|
v1.id = "v1"
|
||||||
|
v1.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'v1', '', bookmark=True),
|
||||||
|
link.Link.make_link('describedby',
|
||||||
|
'http://www.starlingx.io/',
|
||||||
|
'developer/inventory/dev',
|
||||||
|
'api-spec-v1.html',
|
||||||
|
bookmark=True, type='text/html')
|
||||||
|
]
|
||||||
|
v1.media_types = [MediaType('application/json',
|
||||||
|
'application/vnd.openstack.inventory.v1+json')]
|
||||||
|
|
||||||
|
v1.systems = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'systems', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'systems', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.hosts = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'hosts', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'hosts', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.nodes = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'nodes', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.cpus = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'cpus', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'cpus', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.memory = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'memory', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'memory', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.ports = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ports', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ports', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.ethernet_ports = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ethernet_ports', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ethernet_ports', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.lldp_agents = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_agents', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_agents', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.lldp_neighbours = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_neighbours', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_neighbours', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.sensors = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensors', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensors', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
v1.sensorgroups = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensorgroups', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensorgroups', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
return v1
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(rest.RestController):
|
||||||
|
"""Version 1 API controller root."""
|
||||||
|
|
||||||
|
systems = system.SystemController()
|
||||||
|
hosts = host.HostController()
|
||||||
|
nodes = node.NodeController()
|
||||||
|
cpus = cpu.CPUController()
|
||||||
|
memorys = memory.MemoryController()
|
||||||
|
ports = port.PortController()
|
||||||
|
ethernet_ports = ethernet_port.EthernetPortController()
|
||||||
|
lldp_agents = lldp_agent.LLDPAgentController()
|
||||||
|
lldp_neighbours = lldp_neighbour.LLDPNeighbourController()
|
||||||
|
pci_devices = pci_device.PCIDeviceController()
|
||||||
|
sensors = sensor.SensorController()
|
||||||
|
sensorgroups = sensorgroup.SensorGroupController()
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(V1)
|
||||||
|
def get(self):
|
||||||
|
return V1.convert()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('Controller',)
|
|
@ -0,0 +1,130 @@
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import functools
|
||||||
|
from oslo_utils._i18n import _
|
||||||
|
from webob import exc
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
|
||||||
|
class APIBase(wtypes.Base):
|
||||||
|
|
||||||
|
created_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||||
|
"""The time in UTC at which the object is created"""
|
||||||
|
|
||||||
|
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
|
||||||
|
"""The time in UTC at which the object is updated"""
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""Render this object as a dict of its fields."""
|
||||||
|
return dict((k, getattr(self, k))
|
||||||
|
for k in self.fields
|
||||||
|
if hasattr(self, k) and
|
||||||
|
getattr(self, k) != wsme.Unset)
|
||||||
|
|
||||||
|
def unset_fields_except(self, except_list=None):
|
||||||
|
"""Unset fields so they don't appear in the message body.
|
||||||
|
|
||||||
|
:param except_list: A list of fields that won't be touched.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if except_list is None:
|
||||||
|
except_list = []
|
||||||
|
|
||||||
|
for k in self.as_dict():
|
||||||
|
if k not in except_list:
|
||||||
|
setattr(self, k, wsme.Unset)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_rpc_object(cls, m, fields=None):
|
||||||
|
"""Convert a RPC object to an API object."""
|
||||||
|
obj_dict = m.as_dict()
|
||||||
|
# Unset non-required fields so they do not appear
|
||||||
|
# in the message body
|
||||||
|
obj_dict.update(dict((k, wsme.Unset)
|
||||||
|
for k in obj_dict.keys()
|
||||||
|
if fields and k not in fields))
|
||||||
|
return cls(**obj_dict)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
|
class Version(object):
|
||||||
|
"""API Version object."""
|
||||||
|
|
||||||
|
string = 'X-OpenStack-Inventory-API-Version'
|
||||||
|
"""HTTP Header string carrying the requested version"""
|
||||||
|
|
||||||
|
min_string = 'X-OpenStack-Inventory-API-Minimum-Version'
|
||||||
|
"""HTTP response header"""
|
||||||
|
|
||||||
|
max_string = 'X-OpenStack-Inventory-API-Maximum-Version'
|
||||||
|
"""HTTP response header"""
|
||||||
|
|
||||||
|
def __init__(self, headers, default_version, latest_version):
|
||||||
|
"""Create an API Version object from the supplied headers.
|
||||||
|
|
||||||
|
:param headers: webob headers
|
||||||
|
:param default_version: version to use if not specified in headers
|
||||||
|
:param latest_version: version to use if latest is requested
|
||||||
|
:raises: webob.HTTPNotAcceptable
|
||||||
|
"""
|
||||||
|
(self.major, self.minor) = Version.parse_headers(
|
||||||
|
headers, default_version, latest_version)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '%s.%s' % (self.major, self.minor)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_headers(headers, default_version, latest_version):
|
||||||
|
"""Determine the API version requested based on the headers supplied.
|
||||||
|
|
||||||
|
:param headers: webob headers
|
||||||
|
:param default_version: version to use if not specified in headers
|
||||||
|
:param latest_version: version to use if latest is requested
|
||||||
|
:returns: a tupe of (major, minor) version numbers
|
||||||
|
:raises: webob.HTTPNotAcceptable
|
||||||
|
"""
|
||||||
|
version_str = headers.get(Version.string, default_version)
|
||||||
|
|
||||||
|
if version_str.lower() == 'latest':
|
||||||
|
parse_str = latest_version
|
||||||
|
else:
|
||||||
|
parse_str = version_str
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = tuple(int(i) for i in parse_str.split('.'))
|
||||||
|
except ValueError:
|
||||||
|
version = ()
|
||||||
|
|
||||||
|
if len(version) != 2:
|
||||||
|
raise exc.HTTPNotAcceptable(_(
|
||||||
|
"Invalid value for %s header") % Version.string)
|
||||||
|
return version
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return (self.major, self.minor) > (other.major, other.minor)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (self.major, self.minor) == (other.major, other.minor)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
|
@ -0,0 +1,58 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
|
||||||
|
|
||||||
|
class Collection(base.APIBase):
|
||||||
|
|
||||||
|
next = wtypes.text
|
||||||
|
"A link to retrieve the next subset of the collection"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def collection(self):
|
||||||
|
return getattr(self, self._type)
|
||||||
|
|
||||||
|
def has_next(self, limit):
|
||||||
|
"""Return whether collection has more items."""
|
||||||
|
return len(self.collection) and len(self.collection) == limit
|
||||||
|
|
||||||
|
def get_next(self, limit, url=None, **kwargs):
|
||||||
|
"""Return a link to the next subset of the collection."""
|
||||||
|
if not self.has_next(limit):
|
||||||
|
return wtypes.Unset
|
||||||
|
|
||||||
|
resource_url = url or self._type
|
||||||
|
q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs])
|
||||||
|
next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % {
|
||||||
|
'args': q_args, 'limit': limit,
|
||||||
|
'marker': self.collection[-1].uuid}
|
||||||
|
|
||||||
|
return link.Link.make_link('next', pecan.request.host_url,
|
||||||
|
resource_url, next_args).href
|
|
@ -0,0 +1,303 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory import objects
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CPUPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class CPU(base.APIBase):
|
||||||
|
"""API representation of a host CPU.
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of a cpu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this cpu"
|
||||||
|
|
||||||
|
cpu = int
|
||||||
|
"Represent the cpu id cpu"
|
||||||
|
|
||||||
|
core = int
|
||||||
|
"Represent the core id cpu"
|
||||||
|
|
||||||
|
thread = int
|
||||||
|
"Represent the thread id cpu"
|
||||||
|
|
||||||
|
cpu_family = wtypes.text
|
||||||
|
"Represent the cpu family of the cpu"
|
||||||
|
|
||||||
|
cpu_model = wtypes.text
|
||||||
|
"Represent the cpu model of the cpu"
|
||||||
|
|
||||||
|
function = wtypes.text
|
||||||
|
"Represent the function of the cpu"
|
||||||
|
|
||||||
|
num_cores_on_processor0 = wtypes.text
|
||||||
|
"The number of cores on processors 0"
|
||||||
|
|
||||||
|
num_cores_on_processor1 = wtypes.text
|
||||||
|
"The number of cores on processors 1"
|
||||||
|
|
||||||
|
num_cores_on_processor2 = wtypes.text
|
||||||
|
"The number of cores on processors 2"
|
||||||
|
|
||||||
|
num_cores_on_processor3 = wtypes.text
|
||||||
|
"The number of cores on processors 3"
|
||||||
|
|
||||||
|
numa_node = int
|
||||||
|
"The numa node or zone the cpu. API only attribute"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
||||||
|
six.integer_types)}
|
||||||
|
"This cpu's meta data"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"The hostid that this cpu belongs to"
|
||||||
|
|
||||||
|
node_id = int
|
||||||
|
"The nodeId that this cpu belongs to"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"The UUID of the host this cpu belongs to"
|
||||||
|
|
||||||
|
node_uuid = types.uuid
|
||||||
|
"The UUID of the node this cpu belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"A list containing a self link and associated cpu links"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.CPU.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
# API only attributes
|
||||||
|
self.fields.append('function')
|
||||||
|
setattr(self, 'function', kwargs.get('function', None))
|
||||||
|
self.fields.append('num_cores_on_processor0')
|
||||||
|
setattr(self, 'num_cores_on_processor0',
|
||||||
|
kwargs.get('num_cores_on_processor0', None))
|
||||||
|
self.fields.append('num_cores_on_processor1')
|
||||||
|
setattr(self, 'num_cores_on_processor1',
|
||||||
|
kwargs.get('num_cores_on_processor1', None))
|
||||||
|
self.fields.append('num_cores_on_processor2')
|
||||||
|
setattr(self, 'num_cores_on_processor2',
|
||||||
|
kwargs.get('num_cores_on_processor2', None))
|
||||||
|
self.fields.append('num_cores_on_processor3')
|
||||||
|
setattr(self, 'num_cores_on_processor3',
|
||||||
|
kwargs.get('num_cores_on_processor3', None))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_port, expand=True):
|
||||||
|
cpu = CPU(**rpc_port.as_dict())
|
||||||
|
if not expand:
|
||||||
|
cpu.unset_fields_except(
|
||||||
|
['uuid', 'cpu', 'core', 'thread',
|
||||||
|
'cpu_family', 'cpu_model',
|
||||||
|
'numa_node', 'host_uuid', 'node_uuid',
|
||||||
|
'host_id', 'node_id',
|
||||||
|
'capabilities',
|
||||||
|
'created_at', 'updated_at'])
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
cpu.host_id = wtypes.Unset
|
||||||
|
cpu.node_id = wtypes.Unset
|
||||||
|
|
||||||
|
cpu.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'cpus', cpu.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'cpus', cpu.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
return cpu
|
||||||
|
|
||||||
|
|
||||||
|
class CPUCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of cpus."""
|
||||||
|
|
||||||
|
cpus = [CPU]
|
||||||
|
"A list containing cpu objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'cpus'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_ports, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = CPUCollection()
|
||||||
|
collection.cpus = [
|
||||||
|
CPU.convert_with_links(p, expand) for p in rpc_ports]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
class CPUController(rest.RestController):
|
||||||
|
"""REST controller for cpus."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False, from_node=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._from_node = from_node
|
||||||
|
|
||||||
|
def _get_cpus_collection(self, i_uuid, node_uuid, marker,
|
||||||
|
limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not i_uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
if self._from_node and not i_uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Node id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.CPU.get_by_uuid(pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
# cpus = pecan.request.dbapi.cpu_get_by_host(
|
||||||
|
cpus = objects.CPU.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
elif self._from_node:
|
||||||
|
# cpus = pecan.request.dbapi.cpu_get_by_node(
|
||||||
|
cpus = objects.CPU.get_by_node(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
if i_uuid and not node_uuid:
|
||||||
|
# cpus = pecan.request.dbapi.cpu_get_by_host(
|
||||||
|
cpus = objects.CPU.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
elif i_uuid and node_uuid:
|
||||||
|
# cpus = pecan.request.dbapi.cpu_get_by_host_node(
|
||||||
|
cpus = objects.CPU.get_by_host_node(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid,
|
||||||
|
node_uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
elif node_uuid:
|
||||||
|
# cpus = pecan.request.dbapi.cpu_get_by_host_node(
|
||||||
|
cpus = objects.CPU.get_by_node(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid,
|
||||||
|
node_uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# cpus = pecan.request.dbapi.icpu_get_list(
|
||||||
|
cpus = objects.CPU.list(
|
||||||
|
pecan.request.context,
|
||||||
|
limit, marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return CPUCollection.convert_with_links(cpus, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(CPUCollection, types.uuid, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, host_uuid=None, node_uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of cpus."""
|
||||||
|
return self._get_cpus_collection(host_uuid, node_uuid,
|
||||||
|
marker, limit,
|
||||||
|
sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(CPUCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, host_uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of cpus with detail."""
|
||||||
|
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "cpus":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['cpus', 'detail'])
|
||||||
|
return self._get_cpus_collection(host_uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(CPU, types.uuid)
|
||||||
|
def get_one(self, cpu_uuid):
|
||||||
|
"""Retrieve information about the given cpu."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_port = objects.CPU.get_by_uuid(pecan.request.context, cpu_uuid)
|
||||||
|
return CPU.convert_with_links(rpc_port)
|
|
@ -0,0 +1,330 @@
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from inventory.common import constants
|
||||||
|
from inventory.common import k_host
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
CORE_FUNCTIONS = [
|
||||||
|
constants.PLATFORM_FUNCTION,
|
||||||
|
constants.VSWITCH_FUNCTION,
|
||||||
|
constants.SHARED_FUNCTION,
|
||||||
|
constants.VM_FUNCTION,
|
||||||
|
constants.NO_FUNCTION
|
||||||
|
]
|
||||||
|
|
||||||
|
VSWITCH_MIN_CORES = 1
|
||||||
|
VSWITCH_MAX_CORES = 8
|
||||||
|
|
||||||
|
|
||||||
|
class CpuProfile(object):
|
||||||
|
class CpuConfigure(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.platform = 0
|
||||||
|
self.vswitch = 0
|
||||||
|
self.shared = 0
|
||||||
|
self.vms = 0
|
||||||
|
self.numa_node = 0
|
||||||
|
|
||||||
|
# cpus is a list of cpu sorted by numa_node, core and thread
|
||||||
|
# if not, provide a node list sorted by numa_node
|
||||||
|
# (id might not be reliable)
|
||||||
|
def __init__(self, cpus, nodes=None):
|
||||||
|
if nodes is not None:
|
||||||
|
cpus = CpuProfile.sort_cpu_by_numa_node(cpus, nodes)
|
||||||
|
cores = []
|
||||||
|
|
||||||
|
self.number_of_cpu = 0
|
||||||
|
self.cores_per_cpu = 0
|
||||||
|
self.hyper_thread = False
|
||||||
|
self.processors = []
|
||||||
|
cur_processor = None
|
||||||
|
|
||||||
|
for cpu in cpus:
|
||||||
|
key = '{0}-{1}'.format(cpu.numa_node, cpu.core)
|
||||||
|
if key not in cores:
|
||||||
|
cores.append(key)
|
||||||
|
else:
|
||||||
|
self.hyper_thread = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (cur_processor is None or
|
||||||
|
cur_processor.numa_node != cpu.numa_node):
|
||||||
|
cur_processor = CpuProfile.CpuConfigure()
|
||||||
|
cur_processor.numa_node = cpu.numa_node
|
||||||
|
self.processors.append(cur_processor)
|
||||||
|
|
||||||
|
if cpu.allocated_function == constants.PLATFORM_FUNCTION:
|
||||||
|
cur_processor.platform += 1
|
||||||
|
elif cpu.allocated_function == constants.VSWITCH_FUNCTION:
|
||||||
|
cur_processor.vswitch += 1
|
||||||
|
elif cpu.allocated_function == constants.SHARED_FUNCTION:
|
||||||
|
cur_processor.shared += 1
|
||||||
|
elif cpu.allocated_function == constants.VM_FUNCTION:
|
||||||
|
cur_processor.vms += 1
|
||||||
|
|
||||||
|
self.number_of_cpu = len(self.processors)
|
||||||
|
self.cores_per_cpu = len(cores) / self.number_of_cpu
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort_cpu_by_numa_node(cpus, nodes):
|
||||||
|
newlist = []
|
||||||
|
for node in nodes:
|
||||||
|
for cpu in cpus:
|
||||||
|
if cpu.node_id == node.id:
|
||||||
|
cpu.numa_node = node.numa_node
|
||||||
|
newlist.append(cpu)
|
||||||
|
return newlist
|
||||||
|
|
||||||
|
|
||||||
|
class HostCpuProfile(CpuProfile):
|
||||||
|
def __init__(self, subfunctions, cpus, nodes=None):
|
||||||
|
super(HostCpuProfile, self).__init__(cpus, nodes)
|
||||||
|
self.subfunctions = subfunctions
|
||||||
|
|
||||||
|
# see if a cpu profile is applicable to this host
|
||||||
|
def profile_applicable(self, profile):
|
||||||
|
if self.number_of_cpu == profile.number_of_cpu and \
|
||||||
|
self.cores_per_cpu == profile.cores_per_cpu:
|
||||||
|
return self.check_profile_core_functions(profile)
|
||||||
|
return False # Profile is not applicable to host
|
||||||
|
|
||||||
|
def check_profile_core_functions(self, profile):
|
||||||
|
platform_cores = 0
|
||||||
|
vswitch_cores = 0
|
||||||
|
shared_cores = 0
|
||||||
|
vm_cores = 0
|
||||||
|
for cpu in profile.processors:
|
||||||
|
platform_cores += cpu.platform
|
||||||
|
vswitch_cores += cpu.vswitch
|
||||||
|
shared_cores += cpu.shared
|
||||||
|
vm_cores += cpu.vms
|
||||||
|
|
||||||
|
error_string = ""
|
||||||
|
if platform_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.PLATFORM_FUNCTION
|
||||||
|
elif k_host.COMPUTE in self.subfunctions and vswitch_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.VSWITCH_FUNCTION
|
||||||
|
elif k_host.COMPUTE in self.subfunctions and vm_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.VM_FUNCTION
|
||||||
|
return error_string
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_function(s):
|
||||||
|
for f in CORE_FUNCTIONS:
|
||||||
|
if s.lower() == f.lower():
|
||||||
|
return f
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def check_profile_core_functions(personality, profile):
|
||||||
|
|
||||||
|
platform_cores = 0
|
||||||
|
vswitch_cores = 0
|
||||||
|
shared_cores = 0
|
||||||
|
vm_cores = 0
|
||||||
|
for cpu in profile.processors:
|
||||||
|
platform_cores += cpu.platform
|
||||||
|
vswitch_cores += cpu.vswitch
|
||||||
|
shared_cores += cpu.shared
|
||||||
|
vm_cores += cpu.vms
|
||||||
|
|
||||||
|
error_string = ""
|
||||||
|
if platform_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.PLATFORM_FUNCTION
|
||||||
|
elif k_host.COMPUTE in personality and vswitch_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.VSWITCH_FUNCTION
|
||||||
|
elif k_host.COMPUTE in personality and vm_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.VM_FUNCTION
|
||||||
|
return error_string
|
||||||
|
|
||||||
|
|
||||||
|
def check_core_functions(personality, icpus):
|
||||||
|
platform_cores = 0
|
||||||
|
vswitch_cores = 0
|
||||||
|
shared_cores = 0
|
||||||
|
vm_cores = 0
|
||||||
|
for cpu in icpus:
|
||||||
|
allocated_function = cpu.allocated_function
|
||||||
|
if allocated_function == constants.PLATFORM_FUNCTION:
|
||||||
|
platform_cores += 1
|
||||||
|
elif allocated_function == constants.VSWITCH_FUNCTION:
|
||||||
|
vswitch_cores += 1
|
||||||
|
elif allocated_function == constants.SHARED_FUNCTION:
|
||||||
|
shared_cores += 1
|
||||||
|
elif allocated_function == constants.VM_FUNCTION:
|
||||||
|
vm_cores += 1
|
||||||
|
|
||||||
|
error_string = ""
|
||||||
|
if platform_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.PLATFORM_FUNCTION
|
||||||
|
elif k_host.COMPUTE in personality and vswitch_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.VSWITCH_FUNCTION
|
||||||
|
elif k_host.COMPUTE in personality and vm_cores == 0:
|
||||||
|
error_string = "There must be at least one core for %s." % \
|
||||||
|
constants.VM_FUNCTION
|
||||||
|
return error_string
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_function(host):
|
||||||
|
"""Return the default function to be assigned to cpus on this host"""
|
||||||
|
if k_host.COMPUTE in host.subfunctions:
|
||||||
|
return constants.VM_FUNCTION
|
||||||
|
return constants.PLATFORM_FUNCTION
|
||||||
|
|
||||||
|
|
||||||
|
def get_cpu_function(host, cpu):
|
||||||
|
"""Return the function that is assigned to the specified cpu"""
|
||||||
|
for s in range(0, len(host.nodes)):
|
||||||
|
functions = host.cpu_functions[s]
|
||||||
|
for f in CORE_FUNCTIONS:
|
||||||
|
if cpu.cpu in functions[f]:
|
||||||
|
return f
|
||||||
|
return constants.NO_FUNCTION
|
||||||
|
|
||||||
|
|
||||||
|
def get_cpu_counts(host):
|
||||||
|
"""Return the CPU counts for this host by socket and function."""
|
||||||
|
counts = {}
|
||||||
|
for s in range(0, len(host.nodes)):
|
||||||
|
counts[s] = {}
|
||||||
|
for f in CORE_FUNCTIONS:
|
||||||
|
counts[s][f] = len(host.cpu_functions[s][f])
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def init_cpu_counts(host):
|
||||||
|
"""Create empty data structures to track CPU assignments by socket and
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
host.cpu_functions = {}
|
||||||
|
host.cpu_lists = {}
|
||||||
|
for s in range(0, len(host.nodes)):
|
||||||
|
host.cpu_functions[s] = {}
|
||||||
|
for f in CORE_FUNCTIONS:
|
||||||
|
host.cpu_functions[s][f] = []
|
||||||
|
host.cpu_lists[s] = []
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_by_coreid(cpu):
|
||||||
|
"""Sort a list of cpu database objects such that threads of the same core
|
||||||
|
are adjacent in the list with the lowest thread number appearing first.
|
||||||
|
"""
|
||||||
|
return (int(cpu.core), int(cpu.thread))
|
||||||
|
|
||||||
|
|
||||||
|
def restructure_host_cpu_data(host):
|
||||||
|
"""Reorganize the cpu list by socket and function so that it can more
|
||||||
|
easily be consumed by other utilities.
|
||||||
|
"""
|
||||||
|
init_cpu_counts(host)
|
||||||
|
host.sockets = len(host.nodes or [])
|
||||||
|
host.hyperthreading = False
|
||||||
|
host.physical_cores = 0
|
||||||
|
if not host.cpus:
|
||||||
|
return
|
||||||
|
host.cpu_model = host.cpus[0].cpu_model
|
||||||
|
cpu_list = sorted(host.cpus, key=_sort_by_coreid)
|
||||||
|
for cpu in cpu_list:
|
||||||
|
inode = pecan.request.dbapi.inode_get(inode_id=cpu.node_id)
|
||||||
|
cpu.numa_node = inode.numa_node
|
||||||
|
if cpu.thread == 0:
|
||||||
|
host.physical_cores += 1
|
||||||
|
elif cpu.thread > 0:
|
||||||
|
host.hyperthreading = True
|
||||||
|
function = cpu.allocated_function or get_default_function(host)
|
||||||
|
host.cpu_functions[cpu.numa_node][function].append(int(cpu.cpu))
|
||||||
|
host.cpu_lists[cpu.numa_node].append(int(cpu.cpu))
|
||||||
|
|
||||||
|
|
||||||
|
def check_core_allocations(host, cpu_counts, func):
|
||||||
|
"""Check that minimum and maximum core values are respected."""
|
||||||
|
total_platform_cores = 0
|
||||||
|
total_vswitch_cores = 0
|
||||||
|
total_shared_cores = 0
|
||||||
|
for s in range(0, len(host.nodes)):
|
||||||
|
available_cores = len(host.cpu_lists[s])
|
||||||
|
platform_cores = cpu_counts[s][constants.PLATFORM_FUNCTION]
|
||||||
|
vswitch_cores = cpu_counts[s][constants.VSWITCH_FUNCTION]
|
||||||
|
shared_cores = cpu_counts[s][constants.SHARED_FUNCTION]
|
||||||
|
requested_cores = platform_cores + vswitch_cores + shared_cores
|
||||||
|
if requested_cores > available_cores:
|
||||||
|
return ("More total logical cores requested than present on "
|
||||||
|
"'Processor %s' (%s cores)." % (s, available_cores))
|
||||||
|
total_platform_cores += platform_cores
|
||||||
|
total_vswitch_cores += vswitch_cores
|
||||||
|
total_shared_cores += shared_cores
|
||||||
|
if func.lower() == constants.PLATFORM_FUNCTION.lower():
|
||||||
|
if ((k_host.CONTROLLER in host.subfunctions) and
|
||||||
|
(k_host.COMPUTE in host.subfunctions)):
|
||||||
|
if total_platform_cores < 2:
|
||||||
|
return "%s must have at least two cores." % \
|
||||||
|
constants.PLATFORM_FUNCTION
|
||||||
|
elif total_platform_cores == 0:
|
||||||
|
return "%s must have at least one core." % \
|
||||||
|
constants.PLATFORM_FUNCTION
|
||||||
|
if k_host.COMPUTE in (host.subfunctions or host.personality):
|
||||||
|
if func.lower() == constants.VSWITCH_FUNCTION.lower():
|
||||||
|
if host.hyperthreading:
|
||||||
|
total_physical_cores = total_vswitch_cores / 2
|
||||||
|
else:
|
||||||
|
total_physical_cores = total_vswitch_cores
|
||||||
|
if total_physical_cores < VSWITCH_MIN_CORES:
|
||||||
|
return ("The %s function must have at least %s core(s)." %
|
||||||
|
(constants.VSWITCH_FUNCTION.lower(),
|
||||||
|
VSWITCH_MIN_CORES))
|
||||||
|
elif total_physical_cores > VSWITCH_MAX_CORES:
|
||||||
|
return ("The %s function can only be assigned up to %s cores."
|
||||||
|
% (constants.VSWITCH_FUNCTION.lower(),
|
||||||
|
VSWITCH_MAX_CORES))
|
||||||
|
reserved_for_vms = \
|
||||||
|
len(host.cpus) - total_platform_cores - total_vswitch_cores
|
||||||
|
if reserved_for_vms <= 0:
|
||||||
|
return "There must be at least one unused core for %s." % \
|
||||||
|
constants. VM_FUNCTION
|
||||||
|
else:
|
||||||
|
if total_platform_cores != len(host.cpus):
|
||||||
|
return "All logical cores must be reserved for platform use"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def update_core_allocations(host, cpu_counts):
|
||||||
|
"""Update the per socket/function cpu list based on the newly requested
|
||||||
|
counts.
|
||||||
|
"""
|
||||||
|
# Remove any previous assignments
|
||||||
|
for s in range(0, len(host.nodes)):
|
||||||
|
for f in CORE_FUNCTIONS:
|
||||||
|
host.cpu_functions[s][f] = []
|
||||||
|
# Set new assignments
|
||||||
|
for s in range(0, len(host.nodes)):
|
||||||
|
cpu_list = host.cpu_lists[s] if s in host.cpu_lists else []
|
||||||
|
# Reserve for the platform first
|
||||||
|
for i in range(0, cpu_counts[s][constants.PLATFORM_FUNCTION]):
|
||||||
|
host.cpu_functions[s][constants.PLATFORM_FUNCTION].append(
|
||||||
|
cpu_list.pop(0))
|
||||||
|
# Reserve for the vswitch next
|
||||||
|
for i in range(0, cpu_counts[s][constants.VSWITCH_FUNCTION]):
|
||||||
|
host.cpu_functions[s][constants.VSWITCH_FUNCTION].append(
|
||||||
|
cpu_list.pop(0))
|
||||||
|
# Reserve for the shared next
|
||||||
|
for i in range(0, cpu_counts[s][constants.SHARED_FUNCTION]):
|
||||||
|
host.cpu_functions[s][constants.SHARED_FUNCTION].append(
|
||||||
|
cpu_list.pop(0))
|
||||||
|
# Assign the remaining cpus to the default function for this host
|
||||||
|
host.cpu_functions[s][get_default_function(host)] += cpu_list
|
||||||
|
return
|
|
@ -0,0 +1,310 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory import objects
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EthernetPortPatchType(types.JsonPatchType):
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class EthernetPort(base.APIBase):
|
||||||
|
"""API representation of an Ethernet port
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
Ethernet port.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this port"
|
||||||
|
|
||||||
|
type = wtypes.text
|
||||||
|
"Represent the type of port"
|
||||||
|
|
||||||
|
name = wtypes.text
|
||||||
|
"Represent the name of the port. Unique per host"
|
||||||
|
|
||||||
|
namedisplay = wtypes.text
|
||||||
|
"Represent the display name of the port. Unique per host"
|
||||||
|
|
||||||
|
pciaddr = wtypes.text
|
||||||
|
"Represent the pci address of the port"
|
||||||
|
|
||||||
|
dev_id = int
|
||||||
|
"The unique identifier of PCI device"
|
||||||
|
|
||||||
|
pclass = wtypes.text
|
||||||
|
"Represent the pci class of the port"
|
||||||
|
|
||||||
|
pvendor = wtypes.text
|
||||||
|
"Represent the pci vendor of the port"
|
||||||
|
|
||||||
|
pdevice = wtypes.text
|
||||||
|
"Represent the pci device of the port"
|
||||||
|
|
||||||
|
psvendor = wtypes.text
|
||||||
|
"Represent the pci svendor of the port"
|
||||||
|
|
||||||
|
psdevice = wtypes.text
|
||||||
|
"Represent the pci sdevice of the port"
|
||||||
|
|
||||||
|
numa_node = int
|
||||||
|
"Represent the numa node or zone sdevice of the port"
|
||||||
|
|
||||||
|
sriov_totalvfs = int
|
||||||
|
"The total number of available SR-IOV VFs"
|
||||||
|
|
||||||
|
sriov_numvfs = int
|
||||||
|
"The number of configured SR-IOV VFs"
|
||||||
|
|
||||||
|
sriov_vfs_pci_address = wtypes.text
|
||||||
|
"The PCI Addresses of the VFs"
|
||||||
|
|
||||||
|
driver = wtypes.text
|
||||||
|
"The kernel driver for this device"
|
||||||
|
|
||||||
|
mac = wsme.wsattr(types.macaddress, mandatory=False)
|
||||||
|
"Represent the MAC Address of the port"
|
||||||
|
|
||||||
|
mtu = int
|
||||||
|
"Represent the MTU size (bytes) of the port"
|
||||||
|
|
||||||
|
speed = int
|
||||||
|
"Represent the speed (MBytes/sec) of the port"
|
||||||
|
|
||||||
|
link_mode = int
|
||||||
|
"Represent the link mode of the port"
|
||||||
|
|
||||||
|
duplex = wtypes.text
|
||||||
|
"Represent the duplex mode of the port"
|
||||||
|
|
||||||
|
autoneg = wtypes.text
|
||||||
|
"Represent the auto-negotiation mode of the port"
|
||||||
|
|
||||||
|
bootp = wtypes.text
|
||||||
|
"Represent the bootp port of the host"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
||||||
|
six.integer_types)}
|
||||||
|
"Represent meta data of the port"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"Represent the host_id the port belongs to"
|
||||||
|
|
||||||
|
bootif = wtypes.text
|
||||||
|
"Represent whether the port is a boot port"
|
||||||
|
|
||||||
|
dpdksupport = bool
|
||||||
|
"Represent whether or not the port supports DPDK acceleration"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"Represent the UUID of the host the port belongs to"
|
||||||
|
|
||||||
|
node_uuid = types.uuid
|
||||||
|
"Represent the UUID of the node the port belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated port links"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.EthernetPort.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_port, expand=True):
|
||||||
|
port = EthernetPort(**rpc_port.as_dict())
|
||||||
|
if not expand:
|
||||||
|
port.unset_fields_except(['uuid', 'host_id', 'node_id',
|
||||||
|
'type', 'name',
|
||||||
|
'namedisplay', 'pciaddr', 'dev_id',
|
||||||
|
'pclass', 'pvendor', 'pdevice',
|
||||||
|
'psvendor', 'psdevice', 'numa_node',
|
||||||
|
'mac', 'sriov_totalvfs', 'sriov_numvfs',
|
||||||
|
'sriov_vfs_pci_address', 'driver',
|
||||||
|
'mtu', 'speed', 'link_mode',
|
||||||
|
'duplex', 'autoneg', 'bootp',
|
||||||
|
'capabilities',
|
||||||
|
'host_uuid',
|
||||||
|
'node_uuid', 'dpdksupport',
|
||||||
|
'created_at', 'updated_at'])
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
port.host_id = wtypes.Unset
|
||||||
|
port.node_id = wtypes.Unset
|
||||||
|
|
||||||
|
port.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'ethernet_ports', port.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ethernet_ports', port.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
class EthernetPortCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of EthernetPort objects."""
|
||||||
|
|
||||||
|
ethernet_ports = [EthernetPort]
|
||||||
|
"A list containing EthernetPort objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'ethernet_ports'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_ports, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = EthernetPortCollection()
|
||||||
|
collection.ethernet_ports = [EthernetPort.convert_with_links(p, expand)
|
||||||
|
for p in rpc_ports]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'EthernetPortController'
|
||||||
|
|
||||||
|
|
||||||
|
class EthernetPortController(rest.RestController):
|
||||||
|
"""REST controller for EthernetPorts."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False, from_node=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._from_node = from_node
|
||||||
|
|
||||||
|
def _get_ports_collection(self, uuid, node_uuid,
|
||||||
|
marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
if self._from_node and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"node id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.EthernetPort.get_by_uuid(
|
||||||
|
pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
ports = objects.EthernetPort.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit,
|
||||||
|
marker=marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
elif self._from_node:
|
||||||
|
ports = objects.EthernetPort.get_by_numa_node(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit,
|
||||||
|
marker=marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
if uuid:
|
||||||
|
ports = objects.EthernetPort.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit,
|
||||||
|
marker=marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
ports = objects.EthernetPort.list(
|
||||||
|
pecan.request.context,
|
||||||
|
limit, marker=marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return EthernetPortCollection.convert_with_links(
|
||||||
|
ports, limit, url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(EthernetPortCollection, types.uuid, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None, node_uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of ports."""
|
||||||
|
|
||||||
|
return self._get_ports_collection(uuid,
|
||||||
|
node_uuid,
|
||||||
|
marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(EthernetPortCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of ports with detail."""
|
||||||
|
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "ethernet_ports":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['ethernet_ports', 'detail'])
|
||||||
|
return self._get_ports_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(EthernetPort, types.uuid)
|
||||||
|
def get_one(self, port_uuid):
|
||||||
|
"""Retrieve information about the given port."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_port = objects.EthernetPort.get_by_uuid(
|
||||||
|
pecan.request.context, port_uuid)
|
||||||
|
return EthernetPort.convert_with_links(rpc_port)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,58 @@
|
||||||
|
# Copyright 2013 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
|
||||||
|
|
||||||
|
def build_url(resource, resource_args, bookmark=False, base_url=None):
|
||||||
|
if base_url is None:
|
||||||
|
base_url = pecan.request.public_url
|
||||||
|
|
||||||
|
template = '%(url)s/%(res)s' if bookmark else '%(url)s/v1/%(res)s'
|
||||||
|
# FIXME(lucasagomes): I'm getting a 404 when doing a GET on
|
||||||
|
# a nested resource that the URL ends with a '/'.
|
||||||
|
# https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs
|
||||||
|
template += '%(args)s' if resource_args.startswith('?') else '/%(args)s'
|
||||||
|
return template % {'url': base_url, 'res': resource, 'args': resource_args}
|
||||||
|
|
||||||
|
|
||||||
|
class Link(base.APIBase):
|
||||||
|
"""A link representation."""
|
||||||
|
|
||||||
|
href = wtypes.text
|
||||||
|
"""The url of a link."""
|
||||||
|
|
||||||
|
rel = wtypes.text
|
||||||
|
"""The name of a link."""
|
||||||
|
|
||||||
|
type = wtypes.text
|
||||||
|
"""Indicates the type of document/link."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_link(rel_name, url, resource, resource_args,
|
||||||
|
bookmark=False, type=wtypes.Unset):
|
||||||
|
href = build_url(resource, resource_args,
|
||||||
|
bookmark=bookmark, base_url=url)
|
||||||
|
return Link(href=href, rel=rel_name, type=type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = cls(href="http://localhost:18002"
|
||||||
|
"eeaca217-e7d8-47b4-bb41-3f99f20ead81",
|
||||||
|
rel="bookmark")
|
||||||
|
return sample
|
|
@ -0,0 +1,366 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import jsonpatch
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import lldp_tlv
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import k_lldp
|
||||||
|
from inventory.common import utils as cutils
|
||||||
|
from inventory import objects
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPAgentPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPAgent(base.APIBase):
|
||||||
|
"""API representation of an LLDP Agent
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
LLDP agent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this port"
|
||||||
|
|
||||||
|
status = wtypes.text
|
||||||
|
"Represent the status of the lldp agent"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"Represent the host_id the lldp agent belongs to"
|
||||||
|
|
||||||
|
port_id = int
|
||||||
|
"Represent the port_id the lldp agent belongs to"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"Represent the UUID of the host the lldp agent belongs to"
|
||||||
|
|
||||||
|
port_uuid = types.uuid
|
||||||
|
"Represent the UUID of the port the lldp agent belongs to"
|
||||||
|
|
||||||
|
port_name = wtypes.text
|
||||||
|
"Represent the name of the port the lldp neighbour belongs to"
|
||||||
|
|
||||||
|
port_namedisplay = wtypes.text
|
||||||
|
"Represent the display name of the port. Unique per host"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated lldp agent links"
|
||||||
|
|
||||||
|
tlvs = [link.Link]
|
||||||
|
"Links to the collection of LldpNeighbours on this ihost"
|
||||||
|
|
||||||
|
chassis_id = wtypes.text
|
||||||
|
"Represent the status of the lldp agent"
|
||||||
|
|
||||||
|
port_identifier = wtypes.text
|
||||||
|
"Represent the LLDP port id of the lldp agent"
|
||||||
|
|
||||||
|
port_description = wtypes.text
|
||||||
|
"Represent the port description of the lldp agent"
|
||||||
|
|
||||||
|
system_description = wtypes.text
|
||||||
|
"Represent the status of the lldp agent"
|
||||||
|
|
||||||
|
system_name = wtypes.text
|
||||||
|
"Represent the status of the lldp agent"
|
||||||
|
|
||||||
|
system_capabilities = wtypes.text
|
||||||
|
"Represent the status of the lldp agent"
|
||||||
|
|
||||||
|
management_address = wtypes.text
|
||||||
|
"Represent the status of the lldp agent"
|
||||||
|
|
||||||
|
ttl = wtypes.text
|
||||||
|
"Represent the time-to-live of the lldp agent"
|
||||||
|
|
||||||
|
dot1_lag = wtypes.text
|
||||||
|
"Represent the 802.1 link aggregation status of the lldp agent"
|
||||||
|
|
||||||
|
dot1_vlan_names = wtypes.text
|
||||||
|
"Represent the 802.1 vlan names of the lldp agent"
|
||||||
|
|
||||||
|
dot3_mac_status = wtypes.text
|
||||||
|
"Represent the 802.3 MAC/PHY status of the lldp agent"
|
||||||
|
|
||||||
|
dot3_max_frame = wtypes.text
|
||||||
|
"Represent the 802.3 maximum frame size of the lldp agent"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.LLDPAgent.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_lldp_agent, expand=True):
|
||||||
|
lldp_agent = LLDPAgent(**rpc_lldp_agent.as_dict())
|
||||||
|
if not expand:
|
||||||
|
lldp_agent.unset_fields_except([
|
||||||
|
'uuid', 'host_id', 'port_id', 'status', 'host_uuid',
|
||||||
|
'port_uuid', 'port_name', 'port_namedisplay',
|
||||||
|
'created_at', 'updated_at',
|
||||||
|
k_lldp.LLDP_TLV_TYPE_CHASSIS_ID,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_ID,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_TTL,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_NAME,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_DESC,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_CAP,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_MGMT_ADDR,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_DESC,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_LAG,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VLAN_NAMES,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAC_STATUS,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAX_FRAME])
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
lldp_agent.host_id = wtypes.Unset
|
||||||
|
lldp_agent.port_id = wtypes.Unset
|
||||||
|
|
||||||
|
lldp_agent.links = [
|
||||||
|
link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'lldp_agents', lldp_agent.uuid),
|
||||||
|
link.Link.make_link('bookmark', pecan.request.host_url,
|
||||||
|
'lldp_agents', lldp_agent.uuid,
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
|
if expand:
|
||||||
|
lldp_agent.tlvs = [
|
||||||
|
link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_agents',
|
||||||
|
lldp_agent.uuid + "/tlvs"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_agents',
|
||||||
|
lldp_agent.uuid + "/tlvs",
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
|
return lldp_agent
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPAgentCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of LldpAgent objects."""
|
||||||
|
|
||||||
|
lldp_agents = [LLDPAgent]
|
||||||
|
"A list containing LldpAgent objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'lldp_agents'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_lldp_agents, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = LLDPAgentCollection()
|
||||||
|
collection.lldp_agents = [LLDPAgent.convert_with_links(a, expand)
|
||||||
|
for a in rpc_lldp_agents]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'LLDPAgentController'
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPAgentController(rest.RestController):
|
||||||
|
"""REST controller for LldpAgents."""
|
||||||
|
|
||||||
|
tlvs = lldp_tlv.LLDPTLVController(
|
||||||
|
from_lldp_agents=True)
|
||||||
|
"Expose tlvs as a sub-element of LldpAgents"
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False, from_ports=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._from_ports = from_ports
|
||||||
|
|
||||||
|
def _get_lldp_agents_collection(self, uuid,
|
||||||
|
marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_("Host id not specified."))
|
||||||
|
|
||||||
|
if self._from_ports and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_("Port id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.LLDPAgent.get_by_uuid(pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
agents = objects.LLDPAgent.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
elif self._from_ports:
|
||||||
|
agents = []
|
||||||
|
agent = objects.LLDPAgent.get_by_port(pecan.request.context, uuid)
|
||||||
|
agents.append(agent)
|
||||||
|
else:
|
||||||
|
agents = objects.LLDPAgent.list(
|
||||||
|
pecan.request.context,
|
||||||
|
limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return LLDPAgentCollection.convert_with_links(agents, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPAgentCollection, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of lldp agents."""
|
||||||
|
return self._get_lldp_agents_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPAgentCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of lldp_agents with detail."""
|
||||||
|
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "lldp_agents":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['lldp_agents', 'detail'])
|
||||||
|
return self._get_lldp_agents_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPAgent, types.uuid)
|
||||||
|
def get_one(self, port_uuid):
|
||||||
|
"""Retrieve information about the given lldp agent."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_lldp_agent = objects.LLDPAgent.get_by_uuid(
|
||||||
|
pecan.request.context, port_uuid)
|
||||||
|
return LLDPAgent.convert_with_links(rpc_lldp_agent)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(LLDPAgent, body=LLDPAgent)
|
||||||
|
def post(self, agent):
|
||||||
|
"""Create a new lldp agent."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
try:
|
||||||
|
host_uuid = agent.host_uuid
|
||||||
|
port_uuid = agent.port_uuid
|
||||||
|
new_agent = objects.LLDPAgent.create(
|
||||||
|
pecan.request.context,
|
||||||
|
port_uuid,
|
||||||
|
host_uuid,
|
||||||
|
agent.as_dict())
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid data"))
|
||||||
|
return agent.convert_with_links(new_agent)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme.validate(types.uuid, [LLDPAgentPatchType])
|
||||||
|
@wsme_pecan.wsexpose(LLDPAgent, types.uuid,
|
||||||
|
body=[LLDPAgentPatchType])
|
||||||
|
def patch(self, uuid, patch):
|
||||||
|
"""Update an existing lldp agent."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
if self._from_ports:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_agent = objects.LLDPAgent.get_by_uuid(
|
||||||
|
pecan.request.context, uuid)
|
||||||
|
|
||||||
|
# replace ihost_uuid and port_uuid with corresponding
|
||||||
|
patch_obj = jsonpatch.JsonPatch(patch)
|
||||||
|
for p in patch_obj:
|
||||||
|
if p['path'] == '/host_uuid':
|
||||||
|
p['path'] = '/host_id'
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
p['value'])
|
||||||
|
p['value'] = host.id
|
||||||
|
|
||||||
|
if p['path'] == '/port_uuid':
|
||||||
|
p['path'] = '/port_id'
|
||||||
|
try:
|
||||||
|
port = objects.Port.get_by_uuid(
|
||||||
|
pecan.request.context, p['value'])
|
||||||
|
p['value'] = port.id
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
p['value'] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = LLDPAgent(**jsonpatch.apply_patch(rpc_agent.as_dict(),
|
||||||
|
patch_obj))
|
||||||
|
|
||||||
|
except utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Update only the fields that have changed
|
||||||
|
for field in objects.LLDPAgent.fields:
|
||||||
|
if rpc_agent[field] != getattr(agent, field):
|
||||||
|
rpc_agent[field] = getattr(agent, field)
|
||||||
|
|
||||||
|
rpc_agent.save()
|
||||||
|
return LLDPAgent.convert_with_links(rpc_agent)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
|
def delete(self, uuid):
|
||||||
|
"""Delete an lldp agent."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
if self._from_ports:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
pecan.request.dbapi.lldp_agent_destroy(uuid)
|
|
@ -0,0 +1,390 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import jsonpatch
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import lldp_tlv
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import k_lldp
|
||||||
|
from inventory.common import utils as cutils
|
||||||
|
from inventory import objects
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPNeighbourPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPNeighbour(base.APIBase):
|
||||||
|
"""API representation of an LLDP Neighbour
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
LLDP neighbour.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this port"
|
||||||
|
|
||||||
|
msap = wtypes.text
|
||||||
|
"Represent the MAC service access point of the lldp neighbour"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"Represent the host_id the lldp neighbour belongs to"
|
||||||
|
|
||||||
|
port_id = int
|
||||||
|
"Represent the port_id the lldp neighbour belongs to"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"Represent the UUID of the host the lldp neighbour belongs to"
|
||||||
|
|
||||||
|
port_uuid = types.uuid
|
||||||
|
"Represent the UUID of the port the lldp neighbour belongs to"
|
||||||
|
|
||||||
|
port_name = wtypes.text
|
||||||
|
"Represent the name of the port the lldp neighbour belongs to"
|
||||||
|
|
||||||
|
port_namedisplay = wtypes.text
|
||||||
|
"Represent the display name of the port. Unique per host"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated lldp neighbour"
|
||||||
|
"links"
|
||||||
|
|
||||||
|
tlvs = [link.Link]
|
||||||
|
"Links to the collection of LldpNeighbours on this ihost"
|
||||||
|
|
||||||
|
chassis_id = wtypes.text
|
||||||
|
"Represent the status of the lldp neighbour"
|
||||||
|
|
||||||
|
system_description = wtypes.text
|
||||||
|
"Represent the status of the lldp neighbour"
|
||||||
|
|
||||||
|
system_name = wtypes.text
|
||||||
|
"Represent the status of the lldp neighbour"
|
||||||
|
|
||||||
|
system_capabilities = wtypes.text
|
||||||
|
"Represent the status of the lldp neighbour"
|
||||||
|
|
||||||
|
management_address = wtypes.text
|
||||||
|
"Represent the status of the lldp neighbour"
|
||||||
|
|
||||||
|
port_identifier = wtypes.text
|
||||||
|
"Represent the port identifier of the lldp neighbour"
|
||||||
|
|
||||||
|
port_description = wtypes.text
|
||||||
|
"Represent the port description of the lldp neighbour"
|
||||||
|
|
||||||
|
dot1_lag = wtypes.text
|
||||||
|
"Represent the 802.1 link aggregation status of the lldp neighbour"
|
||||||
|
|
||||||
|
dot1_port_vid = wtypes.text
|
||||||
|
"Represent the 802.1 port vlan id of the lldp neighbour"
|
||||||
|
|
||||||
|
dot1_vid_digest = wtypes.text
|
||||||
|
"Represent the 802.1 vlan id digest of the lldp neighbour"
|
||||||
|
|
||||||
|
dot1_management_vid = wtypes.text
|
||||||
|
"Represent the 802.1 management vlan id of the lldp neighbour"
|
||||||
|
|
||||||
|
dot1_vlan_names = wtypes.text
|
||||||
|
"Represent the 802.1 vlan names of the lldp neighbour"
|
||||||
|
|
||||||
|
dot1_proto_vids = wtypes.text
|
||||||
|
"Represent the 802.1 protocol vlan ids of the lldp neighbour"
|
||||||
|
|
||||||
|
dot1_proto_ids = wtypes.text
|
||||||
|
"Represent the 802.1 protocol ids of the lldp neighbour"
|
||||||
|
|
||||||
|
dot3_mac_status = wtypes.text
|
||||||
|
"Represent the 802.3 MAC/PHY status of the lldp neighbour"
|
||||||
|
|
||||||
|
dot3_max_frame = wtypes.text
|
||||||
|
"Represent the 802.3 maximum frame size of the lldp neighbour"
|
||||||
|
|
||||||
|
dot3_power_mdi = wtypes.text
|
||||||
|
"Represent the 802.3 power mdi status of the lldp neighbour"
|
||||||
|
|
||||||
|
ttl = wtypes.text
|
||||||
|
"Represent the neighbour time-to-live"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.LLDPNeighbour.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_lldp_neighbour, expand=True):
|
||||||
|
lldp_neighbour = LLDPNeighbour(**rpc_lldp_neighbour.as_dict())
|
||||||
|
|
||||||
|
if not expand:
|
||||||
|
lldp_neighbour.unset_fields_except([
|
||||||
|
'uuid', 'host_id', 'port_id', 'msap', 'host_uuid', 'port_uuid',
|
||||||
|
'port_name', 'port_namedisplay', 'created_at', 'updated_at',
|
||||||
|
k_lldp.LLDP_TLV_TYPE_CHASSIS_ID,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_ID,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_TTL,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_NAME,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_DESC,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_SYSTEM_CAP,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_MGMT_ADDR,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_PORT_DESC,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_LAG,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PORT_VID,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VID_DIGEST,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_MGMT_VID,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_VIDS,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_PROTO_IDS,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VLAN_NAMES,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT1_VID_DIGEST,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAC_STATUS,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_MAX_FRAME,
|
||||||
|
k_lldp.LLDP_TLV_TYPE_DOT3_POWER_MDI])
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
lldp_neighbour.host_id = wtypes.Unset
|
||||||
|
lldp_neighbour.port_id = wtypes.Unset
|
||||||
|
|
||||||
|
lldp_neighbour.links = [
|
||||||
|
link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'lldp_neighbours', lldp_neighbour.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_neighbours', lldp_neighbour.uuid,
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
|
if expand:
|
||||||
|
lldp_neighbour.tlvs = [
|
||||||
|
link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_neighbours',
|
||||||
|
lldp_neighbour.uuid + "/tlvs"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_neighbours',
|
||||||
|
lldp_neighbour.uuid + "/tlvs",
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
|
return lldp_neighbour
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPNeighbourCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of LldpNeighbour objects."""
|
||||||
|
|
||||||
|
lldp_neighbours = [LLDPNeighbour]
|
||||||
|
"A list containing LldpNeighbour objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'lldp_neighbours'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_lldp_neighbours, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = LLDPNeighbourCollection()
|
||||||
|
|
||||||
|
collection.lldp_neighbours = [LLDPNeighbour.convert_with_links(a,
|
||||||
|
expand)
|
||||||
|
for a in rpc_lldp_neighbours]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'LLDPNeighbourController'
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPNeighbourController(rest.RestController):
|
||||||
|
"""REST controller for LldpNeighbours."""
|
||||||
|
|
||||||
|
tlvs = lldp_tlv.LLDPTLVController(
|
||||||
|
from_lldp_neighbours=True)
|
||||||
|
"Expose tlvs as a sub-element of LldpNeighbours"
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False, from_ports=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._from_ports = from_ports
|
||||||
|
|
||||||
|
def _get_lldp_neighbours_collection(self, uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand=False,
|
||||||
|
resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_("Host id not specified."))
|
||||||
|
|
||||||
|
if self._from_ports and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_("Port id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.LLDPNeighbour.get_by_uuid(
|
||||||
|
pecan.request.context, marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
neighbours = pecan.request.dbapi.lldp_neighbour_get_by_host(
|
||||||
|
uuid, limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
elif self._from_ports:
|
||||||
|
neighbours = pecan.request.dbapi.lldp_neighbour_get_by_port(
|
||||||
|
uuid, limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
neighbours = pecan.request.dbapi.lldp_neighbour_get_list(
|
||||||
|
limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return LLDPNeighbourCollection.convert_with_links(neighbours, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPNeighbourCollection, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of lldp neighbours."""
|
||||||
|
|
||||||
|
return self._get_lldp_neighbours_collection(uuid, marker, limit,
|
||||||
|
sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPNeighbourCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of lldp_neighbours with detail."""
|
||||||
|
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "lldp_neighbours":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['lldp_neighbours', 'detail'])
|
||||||
|
return self._get_lldp_neighbours_collection(uuid, marker, limit,
|
||||||
|
sort_key, sort_dir, expand,
|
||||||
|
resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPNeighbour, types.uuid)
|
||||||
|
def get_one(self, port_uuid):
|
||||||
|
"""Retrieve information about the given lldp neighbour."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_lldp_neighbour = objects.LLDPNeighbour.get_by_uuid(
|
||||||
|
pecan.request.context, port_uuid)
|
||||||
|
return LLDPNeighbour.convert_with_links(rpc_lldp_neighbour)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(LLDPNeighbour, body=LLDPNeighbour)
|
||||||
|
def post(self, neighbour):
|
||||||
|
"""Create a new lldp neighbour."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
try:
|
||||||
|
host_uuid = neighbour.host_uuid
|
||||||
|
port_uuid = neighbour.port_uuid
|
||||||
|
new_neighbour = pecan.request.dbapi.lldp_neighbour_create(
|
||||||
|
port_uuid, host_uuid, neighbour.as_dict())
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid data"))
|
||||||
|
return neighbour.convert_with_links(new_neighbour)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme.validate(types.uuid, [LLDPNeighbourPatchType])
|
||||||
|
@wsme_pecan.wsexpose(LLDPNeighbour, types.uuid,
|
||||||
|
body=[LLDPNeighbourPatchType])
|
||||||
|
def patch(self, uuid, patch):
|
||||||
|
"""Update an existing lldp neighbour."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
if self._from_ports:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_neighbour = objects.LLDPNeighbour.get_by_uuid(
|
||||||
|
pecan.request.context, uuid)
|
||||||
|
|
||||||
|
# replace host_uuid and port_uuid with corresponding
|
||||||
|
patch_obj = jsonpatch.JsonPatch(patch)
|
||||||
|
for p in patch_obj:
|
||||||
|
if p['path'] == '/host_uuid':
|
||||||
|
p['path'] = '/host_id'
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
p['value'])
|
||||||
|
p['value'] = host.id
|
||||||
|
|
||||||
|
if p['path'] == '/port_uuid':
|
||||||
|
p['path'] = '/port_id'
|
||||||
|
try:
|
||||||
|
port = objects.Port.get_by_uuid(
|
||||||
|
pecan.request.context, p['value'])
|
||||||
|
p['value'] = port.id
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
p['value'] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
neighbour = LLDPNeighbour(
|
||||||
|
**jsonpatch.apply_patch(rpc_neighbour.as_dict(), patch_obj))
|
||||||
|
|
||||||
|
except utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Update only the fields that have changed
|
||||||
|
for field in objects.LLDPNeighbour.fields:
|
||||||
|
if rpc_neighbour[field] != getattr(neighbour, field):
|
||||||
|
rpc_neighbour[field] = getattr(neighbour, field)
|
||||||
|
|
||||||
|
rpc_neighbour.save()
|
||||||
|
return LLDPNeighbour.convert_with_links(rpc_neighbour)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
|
def delete(self, uuid):
|
||||||
|
"""Delete an lldp neighbour."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
if self._from_ports:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
pecan.request.dbapi.lldp_neighbour_destroy(uuid)
|
|
@ -0,0 +1,297 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import jsonpatch
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import utils as cutils
|
||||||
|
from inventory import objects
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPTLVPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPTLV(base.APIBase):
|
||||||
|
"""API representation of an LldpTlv
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
LLDP tlv.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type = wtypes.text
|
||||||
|
"Represent the type of the lldp tlv"
|
||||||
|
|
||||||
|
value = wtypes.text
|
||||||
|
"Represent the value of the lldp tlv"
|
||||||
|
|
||||||
|
agent_id = int
|
||||||
|
"Represent the agent_id the lldp tlv belongs to"
|
||||||
|
|
||||||
|
neighbour_id = int
|
||||||
|
"Represent the neighbour the lldp tlv belongs to"
|
||||||
|
|
||||||
|
agent_uuid = types.uuid
|
||||||
|
"Represent the UUID of the agent the lldp tlv belongs to"
|
||||||
|
|
||||||
|
neighbour_uuid = types.uuid
|
||||||
|
"Represent the UUID of the neighbour the lldp tlv belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated lldp tlv links"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.LLDPTLV.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_lldp_tlv, expand=True):
|
||||||
|
lldp_tlv = LLDPTLV(**rpc_lldp_tlv.as_dict())
|
||||||
|
if not expand:
|
||||||
|
lldp_tlv.unset_fields_except(['type', 'value'])
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
lldp_tlv.agent_id = wtypes.Unset
|
||||||
|
lldp_tlv.neighbour_id = wtypes.Unset
|
||||||
|
|
||||||
|
lldp_tlv.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'lldp_tlvs', lldp_tlv.type),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'lldp_tlvs', lldp_tlv.type,
|
||||||
|
bookmark=True)]
|
||||||
|
return lldp_tlv
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPTLVCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of LldpTlv objects."""
|
||||||
|
|
||||||
|
lldp_tlvs = [LLDPTLV]
|
||||||
|
"A list containing LldpTlv objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'lldp_tlvs'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_lldp_tlvs, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = LLDPTLVCollection()
|
||||||
|
collection.lldp_tlvs = [LLDPTLV.convert_with_links(a, expand)
|
||||||
|
for a in rpc_lldp_tlvs]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'LLDPTLVController'
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPTLVController(rest.RestController):
|
||||||
|
"""REST controller for LldpTlvs."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_lldp_agents=False, from_lldp_neighbours=False):
|
||||||
|
self._from_lldp_agents = from_lldp_agents
|
||||||
|
self._from_lldp_neighbours = from_lldp_neighbours
|
||||||
|
|
||||||
|
def _get_lldp_tlvs_collection(self, uuid,
|
||||||
|
marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_lldp_agents and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_("LLDP agent id not specified."))
|
||||||
|
|
||||||
|
if self._from_lldp_neighbours and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_("LLDP neighbour id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.LLDPTLV.get_by_id(pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_lldp_agents:
|
||||||
|
tlvs = objects.LLDPTLV.get_by_agent(pecan.request.context,
|
||||||
|
uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
elif self._from_lldp_neighbours:
|
||||||
|
tlvs = objects.LLDPTLV.get_by_neighbour(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
tlvs = objects.LLDPTLV.list(
|
||||||
|
pecan.request.context,
|
||||||
|
limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return LLDPTLVCollection.convert_with_links(tlvs,
|
||||||
|
limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPTLVCollection, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of lldp tlvs."""
|
||||||
|
return self._get_lldp_tlvs_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPTLVCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of lldp_tlvs with detail."""
|
||||||
|
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "lldp_tlvs":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['lldp_tlvs', 'detail'])
|
||||||
|
return self._get_lldp_tlvs_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(LLDPTLV, int)
|
||||||
|
def get_one(self, id):
|
||||||
|
"""Retrieve information about the given lldp tlv."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_lldp_tlv = objects.LLDPTLV.get_by_id(
|
||||||
|
pecan.request.context, id)
|
||||||
|
return LLDPTLV.convert_with_links(rpc_lldp_tlv)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(LLDPTLV, body=LLDPTLV)
|
||||||
|
def post(self, tlv):
|
||||||
|
"""Create a new lldp tlv."""
|
||||||
|
if self._from_lldp_agents:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
if self._from_lldp_neighbours:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent_uuid = tlv.agent_uuid
|
||||||
|
neighbour_uuid = tlv.neighbour_uuid
|
||||||
|
new_tlv = pecan.request.dbapi.lldp_tlv_create(tlv.as_dict(),
|
||||||
|
agent_uuid,
|
||||||
|
neighbour_uuid)
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid data"))
|
||||||
|
return tlv.convert_with_links(new_tlv)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme.validate(types.uuid, [LLDPTLVPatchType])
|
||||||
|
@wsme_pecan.wsexpose(LLDPTLV, int,
|
||||||
|
body=[LLDPTLVPatchType])
|
||||||
|
def patch(self, id, patch):
|
||||||
|
"""Update an existing lldp tlv."""
|
||||||
|
if self._from_lldp_agents:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
if self._from_lldp_neighbours:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_tlv = objects.LLDPTLV.get_by_id(
|
||||||
|
pecan.request.context, id)
|
||||||
|
|
||||||
|
# replace agent_uuid and neighbour_uuid with corresponding
|
||||||
|
patch_obj = jsonpatch.JsonPatch(patch)
|
||||||
|
for p in patch_obj:
|
||||||
|
if p['path'] == '/agent_uuid':
|
||||||
|
p['path'] = '/agent_id'
|
||||||
|
agent = objects.LLDPAgent.get_by_uuid(pecan.request.context,
|
||||||
|
p['value'])
|
||||||
|
p['value'] = agent.id
|
||||||
|
|
||||||
|
if p['path'] == '/neighbour_uuid':
|
||||||
|
p['path'] = '/neighbour_id'
|
||||||
|
try:
|
||||||
|
neighbour = objects.LLDPNeighbour.get_by_uuid(
|
||||||
|
pecan.request.context, p['value'])
|
||||||
|
p['value'] = neighbour.id
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
p['value'] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
tlv = LLDPTLV(
|
||||||
|
**jsonpatch.apply_patch(rpc_tlv.as_dict(), patch_obj))
|
||||||
|
|
||||||
|
except utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Update only the fields that have changed
|
||||||
|
for field in objects.LLDPTLV.fields:
|
||||||
|
if rpc_tlv[field] != getattr(tlv, field):
|
||||||
|
rpc_tlv[field] = getattr(tlv, field)
|
||||||
|
|
||||||
|
rpc_tlv.save()
|
||||||
|
return LLDPTLV.convert_with_links(rpc_tlv)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(None, int, status_code=204)
|
||||||
|
def delete(self, id):
|
||||||
|
"""Delete an lldp tlv."""
|
||||||
|
if self._from_lldp_agents:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
if self._from_lldp_neighbours:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
tlv = objects.LLDPTLV.get_by_id(pecan.request.context, id)
|
||||||
|
tlv.destroy()
|
||||||
|
# pecan.request.dbapi.lldp_tlv_destroy(id)
|
|
@ -0,0 +1,729 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import jsonpatch
|
||||||
|
import six
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import utils as cutils
|
||||||
|
from inventory import objects
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class Memory(base.APIBase):
|
||||||
|
"""API representation of host memory.
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of a memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_minimum_platform_reserved_mib = None
|
||||||
|
|
||||||
|
def _get_minimum_platform_reserved_mib(self):
|
||||||
|
return self._minimum_platform_reserved_mib
|
||||||
|
|
||||||
|
def _set_minimum_platform_reserved_mib(self, value):
|
||||||
|
if self._minimum_platform_reserved_mib is None:
|
||||||
|
try:
|
||||||
|
ihost = objects.Host.get_by_uuid(pecan.request.context, value)
|
||||||
|
self._minimum_platform_reserved_mib = \
|
||||||
|
cutils.get_minimum_platform_reserved_memory(ihost,
|
||||||
|
self.numa_node)
|
||||||
|
except exception.HostNotFound as e:
|
||||||
|
# Change error code because 404 (NotFound) is inappropriate
|
||||||
|
# response for a POST request to create
|
||||||
|
e.code = 400 # BadRequest
|
||||||
|
raise e
|
||||||
|
elif value == wtypes.Unset:
|
||||||
|
self._minimum_platform_reserved_mib = wtypes.Unset
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this memory"
|
||||||
|
|
||||||
|
memtotal_mib = int
|
||||||
|
"Represent the imemory total in MiB"
|
||||||
|
|
||||||
|
memavail_mib = int
|
||||||
|
"Represent the imemory available in MiB"
|
||||||
|
|
||||||
|
platform_reserved_mib = int
|
||||||
|
"Represent the imemory platform reserved in MiB"
|
||||||
|
|
||||||
|
hugepages_configured = wtypes.text
|
||||||
|
"Represent whether huge pages are configured"
|
||||||
|
|
||||||
|
vswitch_hugepages_size_mib = int
|
||||||
|
"Represent the imemory vswitch huge pages size in MiB"
|
||||||
|
|
||||||
|
vswitch_hugepages_reqd = int
|
||||||
|
"Represent the imemory vswitch required number of hugepages"
|
||||||
|
|
||||||
|
vswitch_hugepages_nr = int
|
||||||
|
"Represent the imemory vswitch number of hugepages"
|
||||||
|
|
||||||
|
vswitch_hugepages_avail = int
|
||||||
|
"Represent the imemory vswitch number of hugepages available"
|
||||||
|
|
||||||
|
vm_hugepages_nr_2M_pending = int
|
||||||
|
"Represent the imemory vm number of hugepages pending (2M pages)"
|
||||||
|
|
||||||
|
vm_hugepages_nr_2M = int
|
||||||
|
"Represent the imemory vm number of hugepages (2M pages)"
|
||||||
|
|
||||||
|
vm_hugepages_avail_2M = int
|
||||||
|
"Represent the imemory vm number of hugepages available (2M pages)"
|
||||||
|
|
||||||
|
vm_hugepages_nr_1G_pending = int
|
||||||
|
"Represent the imemory vm number of hugepages pending (1G pages)"
|
||||||
|
|
||||||
|
vm_hugepages_nr_1G = int
|
||||||
|
"Represent the imemory vm number of hugepages (1G pages)"
|
||||||
|
|
||||||
|
vm_hugepages_nr_4K = int
|
||||||
|
"Represent the imemory vm number of hugepages (4K pages)"
|
||||||
|
|
||||||
|
vm_hugepages_use_1G = wtypes.text
|
||||||
|
"1G hugepage is supported 'True' or not 'False' "
|
||||||
|
|
||||||
|
vm_hugepages_avail_1G = int
|
||||||
|
"Represent the imemory vm number of hugepages available (1G pages)"
|
||||||
|
|
||||||
|
vm_hugepages_possible_2M = int
|
||||||
|
"Represent the total possible number of vm hugepages available (2M pages)"
|
||||||
|
|
||||||
|
vm_hugepages_possible_1G = int
|
||||||
|
"Represent the total possible number of vm hugepages available (1G pages)"
|
||||||
|
|
||||||
|
minimum_platform_reserved_mib = wsme.wsproperty(
|
||||||
|
int,
|
||||||
|
_get_minimum_platform_reserved_mib,
|
||||||
|
_set_minimum_platform_reserved_mib,
|
||||||
|
mandatory=True)
|
||||||
|
"Represent the default platform reserved memory in MiB. API only attribute"
|
||||||
|
|
||||||
|
numa_node = int
|
||||||
|
"The numa node or zone the imemory. API only attribute"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
||||||
|
six.integer_types)}
|
||||||
|
"This memory's meta data"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"The ihostid that this imemory belongs to"
|
||||||
|
|
||||||
|
node_id = int
|
||||||
|
"The nodeId that this imemory belongs to"
|
||||||
|
|
||||||
|
ihost_uuid = types.uuid
|
||||||
|
"The UUID of the ihost this memory belongs to"
|
||||||
|
|
||||||
|
node_uuid = types.uuid
|
||||||
|
"The UUID of the node this memory belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"A list containing a self link and associated memory links"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.Memory.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
# API only attributes
|
||||||
|
self.fields.append('minimum_platform_reserved_mib')
|
||||||
|
setattr(self, 'minimum_platform_reserved_mib',
|
||||||
|
kwargs.get('host_id', None))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_mem, expand=True):
|
||||||
|
# fields = ['uuid', 'address'] if not expand else None
|
||||||
|
# memory = imemory.from_rpc_object(rpc_mem, fields)
|
||||||
|
|
||||||
|
memory = Memory(**rpc_mem.as_dict())
|
||||||
|
if not expand:
|
||||||
|
memory.unset_fields_except(
|
||||||
|
['uuid', 'memtotal_mib', 'memavail_mib',
|
||||||
|
'platform_reserved_mib', 'hugepages_configured',
|
||||||
|
'vswitch_hugepages_size_mib', 'vswitch_hugepages_nr',
|
||||||
|
'vswitch_hugepages_reqd',
|
||||||
|
'vswitch_hugepages_avail',
|
||||||
|
'vm_hugepages_nr_2M',
|
||||||
|
'vm_hugepages_nr_1G', 'vm_hugepages_use_1G',
|
||||||
|
'vm_hugepages_nr_2M_pending',
|
||||||
|
'vm_hugepages_avail_2M',
|
||||||
|
'vm_hugepages_nr_1G_pending',
|
||||||
|
'vm_hugepages_avail_1G',
|
||||||
|
'vm_hugepages_nr_4K',
|
||||||
|
'vm_hugepages_possible_2M', 'vm_hugepages_possible_1G',
|
||||||
|
'numa_node', 'ihost_uuid', 'node_uuid',
|
||||||
|
'host_id', 'node_id',
|
||||||
|
'capabilities',
|
||||||
|
'created_at', 'updated_at',
|
||||||
|
'minimum_platform_reserved_mib'])
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
memory.host_id = wtypes.Unset
|
||||||
|
memory.node_id = wtypes.Unset
|
||||||
|
|
||||||
|
memory.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'memorys', memory.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'memorys', memory.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
return memory
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of memorys."""
|
||||||
|
|
||||||
|
memorys = [Memory]
|
||||||
|
"A list containing memory objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'memorys'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, memorys, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = MemoryCollection()
|
||||||
|
collection.memorys = [
|
||||||
|
Memory.convert_with_links(n, expand) for n in memorys]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'MemoryController'
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryController(rest.RestController):
|
||||||
|
"""REST controller for memorys."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False, from_node=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._from_node = from_node
|
||||||
|
|
||||||
|
def _get_memorys_collection(self, i_uuid, node_uuid,
|
||||||
|
marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not i_uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
if self._from_node and not i_uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Node id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.Memory.get_by_uuid(pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
# memorys = pecan.request.dbapi.imemory_get_by_ihost(
|
||||||
|
memorys = objects.Memory.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
elif self._from_node:
|
||||||
|
# memorys = pecan.request.dbapi.imemory_get_by_node(
|
||||||
|
memorys = objects.Memory.get_by_node(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
if i_uuid and not node_uuid:
|
||||||
|
# memorys = pecan.request.dbapi.imemory_get_by_ihost(
|
||||||
|
memorys = objects.Memory.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
elif i_uuid and node_uuid: # Need ihost_uuid ?
|
||||||
|
# memorys = pecan.request.dbapi.imemory_get_by_ihost_node(
|
||||||
|
memorys = objects.Memory.get_by_host_node(
|
||||||
|
pecan.request.context,
|
||||||
|
i_uuid,
|
||||||
|
node_uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
elif node_uuid:
|
||||||
|
# memorys = pecan.request.dbapi.imemory_get_by_ihost_node(
|
||||||
|
memorys = objects.Memory.get_by_node(
|
||||||
|
pecan.request.context,
|
||||||
|
node_uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
# memorys = pecan.request.dbapi.imemory_get_list(
|
||||||
|
memorys = objects.Memory.list(
|
||||||
|
pecan.request.context,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return MemoryCollection.convert_with_links(memorys, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(MemoryCollection, types.uuid, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, ihost_uuid=None, node_uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of memorys."""
|
||||||
|
|
||||||
|
return self._get_memorys_collection(
|
||||||
|
ihost_uuid, node_uuid, marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(MemoryCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, ihost_uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of memorys with detail."""
|
||||||
|
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "memorys":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['memorys', 'detail'])
|
||||||
|
return self._get_memorys_collection(ihost_uuid, marker, limit,
|
||||||
|
sort_key, sort_dir,
|
||||||
|
expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(Memory, types.uuid)
|
||||||
|
def get_one(self, memory_uuid):
|
||||||
|
"""Retrieve information about the given memory."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_mem = objects.Memory.get_by_uuid(pecan.request.context,
|
||||||
|
memory_uuid)
|
||||||
|
return Memory.convert_with_links(rpc_mem)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(Memory, body=Memory)
|
||||||
|
def post(self, memory):
|
||||||
|
"""Create a new memory."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
try:
|
||||||
|
ihost_uuid = memory.ihost_uuid
|
||||||
|
new_memory = pecan.request.dbapi.imemory_create(ihost_uuid,
|
||||||
|
memory.as_dict())
|
||||||
|
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid data"))
|
||||||
|
return Memory.convert_with_links(new_memory)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme.validate(types.uuid, [MemoryPatchType])
|
||||||
|
@wsme_pecan.wsexpose(Memory, types.uuid,
|
||||||
|
body=[MemoryPatchType])
|
||||||
|
def patch(self, memory_uuid, patch):
|
||||||
|
"""Update an existing memory."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_mem = objects.Memory.get_by_uuid(
|
||||||
|
pecan.request.context, memory_uuid)
|
||||||
|
|
||||||
|
if 'host_id' in rpc_mem:
|
||||||
|
ihostId = rpc_mem['host_id']
|
||||||
|
else:
|
||||||
|
ihostId = rpc_mem['ihost_uuid']
|
||||||
|
|
||||||
|
host_id = pecan.request.dbapi.ihost_get(ihostId)
|
||||||
|
|
||||||
|
vm_hugepages_nr_2M_pending = None
|
||||||
|
vm_hugepages_nr_1G_pending = None
|
||||||
|
platform_reserved_mib = None
|
||||||
|
for p in patch:
|
||||||
|
if p['path'] == '/platform_reserved_mib':
|
||||||
|
platform_reserved_mib = p['value']
|
||||||
|
if p['path'] == '/vm_hugepages_nr_2M_pending':
|
||||||
|
vm_hugepages_nr_2M_pending = p['value']
|
||||||
|
|
||||||
|
if p['path'] == '/vm_hugepages_nr_1G_pending':
|
||||||
|
vm_hugepages_nr_1G_pending = p['value']
|
||||||
|
|
||||||
|
# The host must be locked
|
||||||
|
if host_id:
|
||||||
|
_check_host(host_id)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"Hostname or uuid must be defined"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Semantics checks and update hugepage memory accounting
|
||||||
|
patch = _check_huge_values(
|
||||||
|
rpc_mem, patch,
|
||||||
|
vm_hugepages_nr_2M_pending, vm_hugepages_nr_1G_pending)
|
||||||
|
except wsme.exc.ClientSideError as e:
|
||||||
|
node = pecan.request.dbapi.node_get(node_id=rpc_mem.node_id)
|
||||||
|
numa_node = node.numa_node
|
||||||
|
msg = _('Processor {0}:').format(numa_node) + e.message
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
# Semantics checks for platform memory
|
||||||
|
_check_memory(rpc_mem, host_id, platform_reserved_mib,
|
||||||
|
vm_hugepages_nr_2M_pending, vm_hugepages_nr_1G_pending)
|
||||||
|
|
||||||
|
# only allow patching allocated_function and capabilities
|
||||||
|
# replace ihost_uuid and node_uuid with corresponding
|
||||||
|
patch_obj = jsonpatch.JsonPatch(patch)
|
||||||
|
|
||||||
|
for p in patch_obj:
|
||||||
|
if p['path'] == '/ihost_uuid':
|
||||||
|
p['path'] = '/host_id'
|
||||||
|
ihost = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
p['value'])
|
||||||
|
p['value'] = ihost.id
|
||||||
|
|
||||||
|
if p['path'] == '/node_uuid':
|
||||||
|
p['path'] = '/node_id'
|
||||||
|
try:
|
||||||
|
node = objects.Node.get_by_uuid(
|
||||||
|
pecan.request.context, p['value'])
|
||||||
|
p['value'] = node.id
|
||||||
|
except exception.InventoryException:
|
||||||
|
p['value'] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
memory = Memory(**jsonpatch.apply_patch(rpc_mem.as_dict(),
|
||||||
|
patch_obj))
|
||||||
|
|
||||||
|
except utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Update only the fields that have changed
|
||||||
|
for field in objects.Memory.fields:
|
||||||
|
if rpc_mem[field] != getattr(memory, field):
|
||||||
|
rpc_mem[field] = getattr(memory, field)
|
||||||
|
|
||||||
|
rpc_mem.save()
|
||||||
|
return Memory.convert_with_links(rpc_mem)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
|
def delete(self, memory_uuid):
|
||||||
|
"""Delete a memory."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
pecan.request.dbapi.imemory_destroy(memory_uuid)
|
||||||
|
|
||||||
|
##############
|
||||||
|
# UTILS
|
||||||
|
##############
|
||||||
|
|
||||||
|
|
||||||
|
def _update(mem_uuid, mem_values):
|
||||||
|
|
||||||
|
rpc_mem = objects.Memory.get_by_uuid(pecan.request.context, mem_uuid)
|
||||||
|
if 'host_id' in rpc_mem:
|
||||||
|
ihostId = rpc_mem['host_id']
|
||||||
|
else:
|
||||||
|
ihostId = rpc_mem['ihost_uuid']
|
||||||
|
|
||||||
|
host_id = pecan.request.dbapi.ihost_get(ihostId)
|
||||||
|
|
||||||
|
if 'platform_reserved_mib' in mem_values:
|
||||||
|
platform_reserved_mib = mem_values['platform_reserved_mib']
|
||||||
|
|
||||||
|
if 'vm_hugepages_nr_2M_pending' in mem_values:
|
||||||
|
vm_hugepages_nr_2M_pending = mem_values['vm_hugepages_nr_2M_pending']
|
||||||
|
|
||||||
|
if 'vm_hugepages_nr_1G_pending' in mem_values:
|
||||||
|
vm_hugepages_nr_1G_pending = mem_values['vm_hugepages_nr_1G_pending']
|
||||||
|
|
||||||
|
# The host must be locked
|
||||||
|
if host_id:
|
||||||
|
_check_host(host_id)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError((
|
||||||
|
"Hostname or uuid must be defined"))
|
||||||
|
|
||||||
|
# Semantics checks and update hugepage memory accounting
|
||||||
|
mem_values = _check_huge_values(
|
||||||
|
rpc_mem, mem_values,
|
||||||
|
vm_hugepages_nr_2M_pending, vm_hugepages_nr_1G_pending)
|
||||||
|
|
||||||
|
# Semantics checks for platform memory
|
||||||
|
_check_memory(rpc_mem, host_id, platform_reserved_mib,
|
||||||
|
vm_hugepages_nr_2M_pending, vm_hugepages_nr_1G_pending)
|
||||||
|
|
||||||
|
# update memory values
|
||||||
|
pecan.request.dbapi.imemory_update(mem_uuid, mem_values)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_host(ihost):
|
||||||
|
if utils.is_aio_simplex_host_unlocked(ihost):
|
||||||
|
raise wsme.exc.ClientSideError(_("Host must be locked."))
|
||||||
|
elif ihost['administrative'] != 'locked':
|
||||||
|
unlocked = False
|
||||||
|
current_ihosts = pecan.request.dbapi.ihost_get_list()
|
||||||
|
for h in current_ihosts:
|
||||||
|
if (h['administrative'] != 'locked' and
|
||||||
|
h['hostname'] != ihost['hostname']):
|
||||||
|
unlocked = True
|
||||||
|
if unlocked:
|
||||||
|
raise wsme.exc.ClientSideError(_("Host must be locked."))
|
||||||
|
|
||||||
|
|
||||||
|
def _check_memory(rpc_mem, ihost,
|
||||||
|
platform_reserved_mib=None,
|
||||||
|
vm_hugepages_nr_2M_pending=None,
|
||||||
|
vm_hugepages_nr_1G_pending=None):
|
||||||
|
if platform_reserved_mib:
|
||||||
|
# Check for invalid characters
|
||||||
|
try:
|
||||||
|
val = int(platform_reserved_mib)
|
||||||
|
except ValueError:
|
||||||
|
raise wsme.exc.ClientSideError((
|
||||||
|
"Platform memory must be a number"))
|
||||||
|
if val < 0:
|
||||||
|
raise wsme.exc.ClientSideError((
|
||||||
|
"Platform memory must be greater than zero"))
|
||||||
|
|
||||||
|
# Check for lower limit
|
||||||
|
node_id = rpc_mem['node_id']
|
||||||
|
node = pecan.request.dbapi.node_get(node_id)
|
||||||
|
min_platform_memory = \
|
||||||
|
cutils.get_minimum_platform_reserved_memory(ihost, node.numa_node)
|
||||||
|
if int(platform_reserved_mib) < min_platform_memory:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("Platform reserved memory for numa node {} "
|
||||||
|
"must be greater than the minimum value {}").format(
|
||||||
|
(node.numa_node, min_platform_memory)))
|
||||||
|
|
||||||
|
# Check if it is within 2/3 percent of the total memory
|
||||||
|
node_memtotal_mib = rpc_mem['node_memtotal_mib']
|
||||||
|
max_platform_reserved = node_memtotal_mib * 2 / 3
|
||||||
|
if int(platform_reserved_mib) > max_platform_reserved:
|
||||||
|
low_core = cutils.is_low_core_system(ihost, pecan.request.dbapi)
|
||||||
|
required_platform_reserved = \
|
||||||
|
cutils.get_required_platform_reserved_memory(
|
||||||
|
ihost, node.numa_node, low_core)
|
||||||
|
msg_platform_over = (
|
||||||
|
_("Platform reserved memory {} MiB on node {} "
|
||||||
|
"is not within range [{}, {}]").format(
|
||||||
|
(int(platform_reserved_mib),
|
||||||
|
node.numa_node,
|
||||||
|
required_platform_reserved,
|
||||||
|
max_platform_reserved)))
|
||||||
|
|
||||||
|
if cutils.is_virtual() or cutils.is_virtual_compute(ihost):
|
||||||
|
LOG.warn(msg_platform_over)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(msg_platform_over)
|
||||||
|
|
||||||
|
# Check if it is within the total amount of memory
|
||||||
|
mem_alloc = 0
|
||||||
|
if vm_hugepages_nr_2M_pending:
|
||||||
|
mem_alloc += int(vm_hugepages_nr_2M_pending) * 2
|
||||||
|
elif rpc_mem['vm_hugepages_nr_2M']:
|
||||||
|
mem_alloc += int(rpc_mem['vm_hugepages_nr_2M']) * 2
|
||||||
|
if vm_hugepages_nr_1G_pending:
|
||||||
|
mem_alloc += int(vm_hugepages_nr_1G_pending) * 1000
|
||||||
|
elif rpc_mem['vm_hugepages_nr_1G']:
|
||||||
|
mem_alloc += int(rpc_mem['vm_hugepages_nr_1G']) * 1000
|
||||||
|
LOG.debug("vm total=%s" % (mem_alloc))
|
||||||
|
|
||||||
|
vs_hp_size = rpc_mem['vswitch_hugepages_size_mib']
|
||||||
|
vs_hp_nr = rpc_mem['vswitch_hugepages_nr']
|
||||||
|
mem_alloc += vs_hp_size * vs_hp_nr
|
||||||
|
LOG.debug("vs_hp_nr=%s vs_hp_size=%s" % (vs_hp_nr, vs_hp_size))
|
||||||
|
LOG.debug("memTotal %s mem_alloc %s" % (node_memtotal_mib, mem_alloc))
|
||||||
|
|
||||||
|
# Initial configuration defaults mem_alloc to consume 100% of 2M pages,
|
||||||
|
# so we may marginally exceed available non-huge memory.
|
||||||
|
# Note there will be some variability in total available memory,
|
||||||
|
# so we need to allow some tolerance so we do not hit the limit.
|
||||||
|
avail = node_memtotal_mib - mem_alloc
|
||||||
|
delta = int(platform_reserved_mib) - avail
|
||||||
|
mem_thresh = 32
|
||||||
|
if int(platform_reserved_mib) > avail + mem_thresh:
|
||||||
|
msg = (_("Platform reserved memory {} MiB exceeds {} MiB "
|
||||||
|
"available by {} MiB (2M: {} pages; 1G: {} pages). "
|
||||||
|
"total memory={} MiB, allocated={} MiB.").format(
|
||||||
|
(platform_reserved_mib, avail,
|
||||||
|
delta, delta / 2, delta / 1024,
|
||||||
|
node_memtotal_mib, mem_alloc)))
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
else:
|
||||||
|
msg = (_("Platform reserved memory {} MiB, {} MiB available, "
|
||||||
|
"total memory={} MiB, allocated={} MiB.").format(
|
||||||
|
platform_reserved_mib, avail,
|
||||||
|
node_memtotal_mib, mem_alloc))
|
||||||
|
LOG.info(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_huge_values(rpc_mem, patch, vm_hugepages_nr_2M=None,
|
||||||
|
vm_hugepages_nr_1G=None):
|
||||||
|
|
||||||
|
if rpc_mem['vm_hugepages_use_1G'] == 'False' and vm_hugepages_nr_1G:
|
||||||
|
# cannot provision 1G huge pages if the processor does not support them
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"Processor does not support 1G huge pages."))
|
||||||
|
|
||||||
|
# Check for invalid characters
|
||||||
|
if vm_hugepages_nr_2M:
|
||||||
|
try:
|
||||||
|
val = int(vm_hugepages_nr_2M)
|
||||||
|
except ValueError:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"VM huge pages 2M must be a number"))
|
||||||
|
if int(vm_hugepages_nr_2M) < 0:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"VM huge pages 2M must be greater than or equal to zero"))
|
||||||
|
|
||||||
|
if vm_hugepages_nr_1G:
|
||||||
|
try:
|
||||||
|
val = int(vm_hugepages_nr_1G)
|
||||||
|
except ValueError:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"VM huge pages 1G must be a number"))
|
||||||
|
if val < 0:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"VM huge pages 1G must be greater than or equal to zero"))
|
||||||
|
|
||||||
|
# Check to make sure that the huge pages aren't over committed
|
||||||
|
if rpc_mem['vm_hugepages_possible_2M'] is None and vm_hugepages_nr_2M:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"No available space for 2M huge page allocation"))
|
||||||
|
|
||||||
|
if rpc_mem['vm_hugepages_possible_1G'] is None and vm_hugepages_nr_1G:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"No available space for 1G huge page allocation"))
|
||||||
|
|
||||||
|
# Update the number of available huge pages
|
||||||
|
num_2M_for_1G = 512
|
||||||
|
|
||||||
|
# None == unchanged
|
||||||
|
if vm_hugepages_nr_1G is not None:
|
||||||
|
new_1G_pages = int(vm_hugepages_nr_1G)
|
||||||
|
elif rpc_mem['vm_hugepages_nr_1G_pending']:
|
||||||
|
new_1G_pages = int(rpc_mem['vm_hugepages_nr_1G_pending'])
|
||||||
|
elif rpc_mem['vm_hugepages_nr_1G']:
|
||||||
|
new_1G_pages = int(rpc_mem['vm_hugepages_nr_1G'])
|
||||||
|
else:
|
||||||
|
new_1G_pages = 0
|
||||||
|
|
||||||
|
# None == unchanged
|
||||||
|
if vm_hugepages_nr_2M is not None:
|
||||||
|
new_2M_pages = int(vm_hugepages_nr_2M)
|
||||||
|
elif rpc_mem['vm_hugepages_nr_2M_pending']:
|
||||||
|
new_2M_pages = int(rpc_mem['vm_hugepages_nr_2M_pending'])
|
||||||
|
elif rpc_mem['vm_hugepages_nr_2M']:
|
||||||
|
new_2M_pages = int(rpc_mem['vm_hugepages_nr_2M'])
|
||||||
|
else:
|
||||||
|
new_2M_pages = 0
|
||||||
|
|
||||||
|
LOG.debug('new 2M pages: %s, 1G pages: %s' % (new_2M_pages, new_1G_pages))
|
||||||
|
vm_possible_2M = 0
|
||||||
|
vm_possible_1G = 0
|
||||||
|
if rpc_mem['vm_hugepages_possible_2M']:
|
||||||
|
vm_possible_2M = int(rpc_mem['vm_hugepages_possible_2M'])
|
||||||
|
|
||||||
|
if rpc_mem['vm_hugepages_possible_1G']:
|
||||||
|
vm_possible_1G = int(rpc_mem['vm_hugepages_possible_1G'])
|
||||||
|
|
||||||
|
LOG.debug("max possible 2M pages: %s, max possible 1G pages: %s" %
|
||||||
|
(vm_possible_2M, vm_possible_1G))
|
||||||
|
|
||||||
|
if vm_possible_2M < new_2M_pages:
|
||||||
|
msg = _("No available space for 2M huge page allocation, "
|
||||||
|
"max 2M pages: %d") % vm_possible_2M
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
if vm_possible_1G < new_1G_pages:
|
||||||
|
msg = _("No available space for 1G huge page allocation, "
|
||||||
|
"max 1G pages: %d") % vm_possible_1G
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
# always use vm_possible_2M to compare,
|
||||||
|
if vm_possible_2M < (new_2M_pages + new_1G_pages * num_2M_for_1G):
|
||||||
|
max_1G = int((vm_possible_2M - new_2M_pages) / num_2M_for_1G)
|
||||||
|
max_2M = vm_possible_2M - new_1G_pages * num_2M_for_1G
|
||||||
|
if new_2M_pages > 0 and new_1G_pages > 0:
|
||||||
|
msg = _("No available space for new settings."
|
||||||
|
"Max 1G pages is {} when 2M is {}, or "
|
||||||
|
"Max 2M pages is %s when 1G is {}.").format(
|
||||||
|
max_1G, new_2M_pages, max_2M, new_1G_pages)
|
||||||
|
elif new_1G_pages > 0:
|
||||||
|
msg = _("No available space for 1G huge page allocation, "
|
||||||
|
"max 1G pages: %d") % vm_possible_1G
|
||||||
|
else:
|
||||||
|
msg = _("No available space for 2M huge page allocation, "
|
||||||
|
"max 2M pages: %d") % vm_possible_2M
|
||||||
|
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
return patch
|
|
@ -0,0 +1,261 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import cpu
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import memory
|
||||||
|
from inventory.api.controllers.v1 import port
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory import objects
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NodePatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return ['/address', '/host_uuid']
|
||||||
|
|
||||||
|
|
||||||
|
class Node(base.APIBase):
|
||||||
|
"""API representation of a host node.
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of
|
||||||
|
an node.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this node"
|
||||||
|
|
||||||
|
numa_node = int
|
||||||
|
"numa node zone for this node"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
||||||
|
six.integer_types)}
|
||||||
|
"This node's meta data"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"The hostid that this node belongs to"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"The UUID of the host this node belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"A list containing a self link and associated node links"
|
||||||
|
|
||||||
|
icpus = [link.Link]
|
||||||
|
"Links to the collection of cpus on this node"
|
||||||
|
|
||||||
|
imemorys = [link.Link]
|
||||||
|
"Links to the collection of memorys on this node"
|
||||||
|
|
||||||
|
ports = [link.Link]
|
||||||
|
"Links to the collection of ports on this node"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.Node.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_node, expand=True):
|
||||||
|
minimum_fields = ['uuid', 'numa_node', 'capabilities',
|
||||||
|
'host_uuid', 'host_id',
|
||||||
|
'created_at'] if not expand else None
|
||||||
|
fields = minimum_fields if not expand else None
|
||||||
|
|
||||||
|
node = Node.from_rpc_object(rpc_node, fields)
|
||||||
|
|
||||||
|
# never expose the host_id attribute
|
||||||
|
node.host_id = wtypes.Unset
|
||||||
|
|
||||||
|
node.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'nodes', node.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes', node.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
if expand:
|
||||||
|
node.icpus = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes',
|
||||||
|
node.uuid + "/cpus"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes',
|
||||||
|
node.uuid + "/cpus",
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
node.imemorys = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes',
|
||||||
|
node.uuid + "/memorys"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes',
|
||||||
|
node.uuid + "/memorys",
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
node.ports = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes',
|
||||||
|
node.uuid + "/ports"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'nodes',
|
||||||
|
node.uuid + "/ports",
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class NodeCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of nodes."""
|
||||||
|
|
||||||
|
nodes = [Node]
|
||||||
|
"A list containing node objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'nodes'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_nodes, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = NodeCollection()
|
||||||
|
collection.nodes = [Node.convert_with_links(p, expand)
|
||||||
|
for p in rpc_nodes]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'NodeController'
|
||||||
|
|
||||||
|
|
||||||
|
class NodeController(rest.RestController):
|
||||||
|
"""REST controller for nodes."""
|
||||||
|
|
||||||
|
icpus = cpu.CPUController(from_node=True)
|
||||||
|
"Expose cpus as a sub-element of nodes"
|
||||||
|
|
||||||
|
imemorys = memory.MemoryController(from_node=True)
|
||||||
|
"Expose memorys as a sub-element of nodes"
|
||||||
|
|
||||||
|
ports = port.PortController(from_node=True)
|
||||||
|
"Expose ports as a sub-element of nodes"
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
|
||||||
|
def _get_nodes_collection(self, host_uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand=False, resource_url=None):
|
||||||
|
if self._from_hosts and not host_uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.Node.get_by_uuid(pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if host_uuid:
|
||||||
|
nodes = objects.Node.get_by_host(pecan.request.context,
|
||||||
|
host_uuid,
|
||||||
|
limit,
|
||||||
|
marker=marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
nodes = objects.Node.list(pecan.request.context,
|
||||||
|
limit,
|
||||||
|
marker=marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return NodeCollection.convert_with_links(nodes, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(NodeCollection,
|
||||||
|
types.uuid, types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, host_uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of nodes."""
|
||||||
|
|
||||||
|
return self._get_nodes_collection(host_uuid, marker, limit,
|
||||||
|
sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(NodeCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, host_uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of nodes with detail."""
|
||||||
|
# NOTE(lucasagomes): /detail should only work agaist collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "nodes":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['nodes', 'detail'])
|
||||||
|
return self._get_nodes_collection(host_uuid,
|
||||||
|
marker, limit,
|
||||||
|
sort_key, sort_dir,
|
||||||
|
expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(Node, types.uuid)
|
||||||
|
def get_one(self, node_uuid):
|
||||||
|
"""Retrieve information about the given node."""
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_node = objects.Node.get_by_uuid(pecan.request.context, node_uuid)
|
||||||
|
return Node.convert_with_links(rpc_node)
|
|
@ -0,0 +1,313 @@
|
||||||
|
# Copyright (c) 2015-2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import jsonpatch
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import k_host
|
||||||
|
from inventory.common import utils as cutils
|
||||||
|
from inventory import objects
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PCIDevicePatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class PCIDevice(base.APIBase):
|
||||||
|
"""API representation of an PCI device
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
Pci Device .
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this device"
|
||||||
|
|
||||||
|
type = wtypes.text
|
||||||
|
"Represent the type of device"
|
||||||
|
|
||||||
|
name = wtypes.text
|
||||||
|
"Represent the name of the device. Unique per host"
|
||||||
|
|
||||||
|
pciaddr = wtypes.text
|
||||||
|
"Represent the pci address of the device"
|
||||||
|
|
||||||
|
pclass_id = wtypes.text
|
||||||
|
"Represent the numerical pci class of the device"
|
||||||
|
|
||||||
|
pvendor_id = wtypes.text
|
||||||
|
"Represent the numerical pci vendor of the device"
|
||||||
|
|
||||||
|
pdevice_id = wtypes.text
|
||||||
|
"Represent the numerical pci device of the device"
|
||||||
|
|
||||||
|
pclass = wtypes.text
|
||||||
|
"Represent the pci class description of the device"
|
||||||
|
|
||||||
|
pvendor = wtypes.text
|
||||||
|
"Represent the pci vendor description of the device"
|
||||||
|
|
||||||
|
pdevice = wtypes.text
|
||||||
|
"Represent the pci device description of the device"
|
||||||
|
|
||||||
|
psvendor = wtypes.text
|
||||||
|
"Represent the pci svendor of the device"
|
||||||
|
|
||||||
|
psdevice = wtypes.text
|
||||||
|
"Represent the pci sdevice of the device"
|
||||||
|
|
||||||
|
numa_node = int
|
||||||
|
"Represent the numa node or zone sdevice of the device"
|
||||||
|
|
||||||
|
sriov_totalvfs = int
|
||||||
|
"The total number of available SR-IOV VFs"
|
||||||
|
|
||||||
|
sriov_numvfs = int
|
||||||
|
"The number of configured SR-IOV VFs"
|
||||||
|
|
||||||
|
sriov_vfs_pci_address = wtypes.text
|
||||||
|
"The PCI Addresses of the VFs"
|
||||||
|
|
||||||
|
driver = wtypes.text
|
||||||
|
"The kernel driver for this device"
|
||||||
|
|
||||||
|
extra_info = wtypes.text
|
||||||
|
"Extra information for this device"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"Represent the host_id the device belongs to"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"Represent the UUID of the host the device belongs to"
|
||||||
|
|
||||||
|
enabled = types.boolean
|
||||||
|
"Represent the enabled status of the device"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated device links"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.PCIDevice.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_device, expand=True):
|
||||||
|
device = PCIDevice(**rpc_device.as_dict())
|
||||||
|
if not expand:
|
||||||
|
device.unset_fields_except(['uuid', 'host_id',
|
||||||
|
'name', 'pciaddr', 'pclass_id',
|
||||||
|
'pvendor_id', 'pdevice_id', 'pclass',
|
||||||
|
'pvendor', 'pdevice', 'psvendor',
|
||||||
|
'psdevice', 'numa_node',
|
||||||
|
'sriov_totalvfs', 'sriov_numvfs',
|
||||||
|
'sriov_vfs_pci_address', 'driver',
|
||||||
|
'host_uuid', 'enabled',
|
||||||
|
'created_at', 'updated_at'])
|
||||||
|
|
||||||
|
# do not expose the id attribute
|
||||||
|
device.host_id = wtypes.Unset
|
||||||
|
device.node_id = wtypes.Unset
|
||||||
|
|
||||||
|
device.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'pci_devices', device.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'pci_devices', device.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
class PCIDeviceCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of PciDevice objects."""
|
||||||
|
|
||||||
|
pci_devices = [PCIDevice]
|
||||||
|
"A list containing PciDevice objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'pci_devices'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_devices, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = PCIDeviceCollection()
|
||||||
|
collection.pci_devices = [PCIDevice.convert_with_links(d, expand)
|
||||||
|
for d in rpc_devices]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'PCIDeviceController'
|
||||||
|
|
||||||
|
|
||||||
|
class PCIDeviceController(rest.RestController):
|
||||||
|
"""REST controller for PciDevices."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
|
||||||
|
def _get_pci_devices_collection(self, uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand=False, resource_url=None):
|
||||||
|
if self._from_hosts and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.PCIDevice.get_by_uuid(
|
||||||
|
pecan.request.context,
|
||||||
|
marker)
|
||||||
|
if self._from_hosts:
|
||||||
|
# devices = pecan.request.dbapi.pci_device_get_by_host(
|
||||||
|
devices = objects.PCIDevice.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
if uuid:
|
||||||
|
# devices = pecan.request.dbapi.pci_device_get_by_host(
|
||||||
|
devices = objects.PCIDevice.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
# devices = pecan.request.dbapi.pci_device_get_list(
|
||||||
|
devices = objects.PCIDevice.list(
|
||||||
|
pecan.request.context,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return PCIDeviceCollection.convert_with_links(devices, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(PCIDeviceCollection, types.uuid, types.uuid,
|
||||||
|
int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of devices."""
|
||||||
|
return self._get_pci_devices_collection(
|
||||||
|
uuid, marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(PCIDeviceCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of devices with detail."""
|
||||||
|
|
||||||
|
# NOTE: /detail should only work against collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "pci_devices":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['pci_devices', 'detail'])
|
||||||
|
return self._get_pci_devices_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(PCIDevice, types.uuid)
|
||||||
|
def get_one(self, device_uuid):
|
||||||
|
"""Retrieve information about the given device."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_device = objects.PCIDevice.get_by_uuid(
|
||||||
|
pecan.request.context, device_uuid)
|
||||||
|
return PCIDevice.convert_with_links(rpc_device)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme.validate(types.uuid, [PCIDevicePatchType])
|
||||||
|
@wsme_pecan.wsexpose(PCIDevice, types.uuid,
|
||||||
|
body=[PCIDevicePatchType])
|
||||||
|
def patch(self, device_uuid, patch):
|
||||||
|
"""Update an existing device."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_device = objects.PCIDevice.get_by_uuid(
|
||||||
|
pecan.request.context, device_uuid)
|
||||||
|
|
||||||
|
# replace host_uuid and with corresponding
|
||||||
|
patch_obj = jsonpatch.JsonPatch(patch)
|
||||||
|
for p in patch_obj:
|
||||||
|
if p['path'] == '/host_uuid':
|
||||||
|
p['path'] = '/host_id'
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
p['value'])
|
||||||
|
p['value'] = host.id
|
||||||
|
|
||||||
|
try:
|
||||||
|
device = PCIDevice(**jsonpatch.apply_patch(rpc_device.as_dict(),
|
||||||
|
patch_obj))
|
||||||
|
|
||||||
|
except utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Semantic checks
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
device.host_id)
|
||||||
|
_check_host(host)
|
||||||
|
|
||||||
|
# Update fields that have changed
|
||||||
|
for field in objects.PCIDevice.fields:
|
||||||
|
if rpc_device[field] != getattr(device, field):
|
||||||
|
_check_field(field)
|
||||||
|
rpc_device[field] = getattr(device, field)
|
||||||
|
|
||||||
|
rpc_device.save()
|
||||||
|
return PCIDevice.convert_with_links(rpc_device)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_host(host):
|
||||||
|
if utils.is_aio_simplex_host_unlocked(host):
|
||||||
|
raise wsme.exc.ClientSideError(_('Host must be locked.'))
|
||||||
|
elif host.administrative != k_host.ADMIN_LOCKED and not \
|
||||||
|
utils.is_host_simplex_controller(host):
|
||||||
|
raise wsme.exc.ClientSideError(_('Host must be locked.'))
|
||||||
|
if k_host.COMPUTE not in host.subfunctions:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_('Can only modify compute node cores.'))
|
||||||
|
|
||||||
|
|
||||||
|
def _check_field(field):
|
||||||
|
if field not in ["enabled", "name"]:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_('Modifying %s attribute restricted') % field)
|
|
@ -0,0 +1,334 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import lldp_agent
|
||||||
|
from inventory.api.controllers.v1 import lldp_neighbour
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory import objects
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PortPatchType(types.JsonPatchType):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class Port(base.APIBase):
|
||||||
|
"""API representation of a host port
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
port.
|
||||||
|
"""
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this port"
|
||||||
|
|
||||||
|
type = wtypes.text
|
||||||
|
"Represent the type of port"
|
||||||
|
|
||||||
|
name = wtypes.text
|
||||||
|
"Represent the name of the port. Unique per host"
|
||||||
|
|
||||||
|
namedisplay = wtypes.text
|
||||||
|
"Represent the display name of the port. Unique per host"
|
||||||
|
|
||||||
|
pciaddr = wtypes.text
|
||||||
|
"Represent the pci address of the port"
|
||||||
|
|
||||||
|
dev_id = int
|
||||||
|
"The unique identifier of PCI device"
|
||||||
|
|
||||||
|
pclass = wtypes.text
|
||||||
|
"Represent the pci class of the port"
|
||||||
|
|
||||||
|
pvendor = wtypes.text
|
||||||
|
"Represent the pci vendor of the port"
|
||||||
|
|
||||||
|
pdevice = wtypes.text
|
||||||
|
"Represent the pci device of the port"
|
||||||
|
|
||||||
|
psvendor = wtypes.text
|
||||||
|
"Represent the pci svendor of the port"
|
||||||
|
|
||||||
|
psdevice = wtypes.text
|
||||||
|
"Represent the pci sdevice of the port"
|
||||||
|
|
||||||
|
numa_node = int
|
||||||
|
"Represent the numa node or zone sdevice of the port"
|
||||||
|
|
||||||
|
sriov_totalvfs = int
|
||||||
|
"The total number of available SR-IOV VFs"
|
||||||
|
|
||||||
|
sriov_numvfs = int
|
||||||
|
"The number of configured SR-IOV VFs"
|
||||||
|
|
||||||
|
sriov_vfs_pci_address = wtypes.text
|
||||||
|
"The PCI Addresses of the VFs"
|
||||||
|
|
||||||
|
driver = wtypes.text
|
||||||
|
"The kernel driver for this device"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
||||||
|
six.integer_types)}
|
||||||
|
"Represent meta data of the port"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"Represent the host_id the port belongs to"
|
||||||
|
|
||||||
|
interface_id = int
|
||||||
|
"Represent the interface_id the port belongs to"
|
||||||
|
|
||||||
|
dpdksupport = bool
|
||||||
|
"Represent whether or not the port supports DPDK acceleration"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"Represent the UUID of the host the port belongs to"
|
||||||
|
|
||||||
|
interface_uuid = types.uuid
|
||||||
|
"Represent the UUID of the interface the port belongs to"
|
||||||
|
|
||||||
|
node_uuid = types.uuid
|
||||||
|
"Represent the UUID of the node the port belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated port links"
|
||||||
|
|
||||||
|
lldp_agents = [link.Link]
|
||||||
|
"Links to the collection of LldpAgents on this port"
|
||||||
|
|
||||||
|
lldp_neighbours = [link.Link]
|
||||||
|
"Links to the collection of LldpNeighbours on this port"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.Port.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_port, expand=True):
|
||||||
|
port = Port(**rpc_port.as_dict())
|
||||||
|
if not expand:
|
||||||
|
port.unset_fields_except(['uuid', 'host_id', 'node_id',
|
||||||
|
'interface_id', 'type', 'name',
|
||||||
|
'namedisplay', 'pciaddr', 'dev_id',
|
||||||
|
'pclass', 'pvendor', 'pdevice',
|
||||||
|
'psvendor', 'psdevice', 'numa_node',
|
||||||
|
'sriov_totalvfs', 'sriov_numvfs',
|
||||||
|
'sriov_vfs_pci_address', 'driver',
|
||||||
|
'capabilities',
|
||||||
|
'host_uuid', 'interface_uuid',
|
||||||
|
'node_uuid', 'dpdksupport',
|
||||||
|
'created_at', 'updated_at'])
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
port.host_id = wtypes.Unset
|
||||||
|
port.interface_id = wtypes.Unset
|
||||||
|
port.node_id = wtypes.Unset
|
||||||
|
|
||||||
|
port.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'ports', port.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ports', port.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
port.lldp_agents = [link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ports',
|
||||||
|
port.uuid + "/lldp_agents"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ports',
|
||||||
|
port.uuid + "/lldp_agents",
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
port.lldp_neighbours = [
|
||||||
|
link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ports',
|
||||||
|
port.uuid + "/lldp_neighbors"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'ports',
|
||||||
|
port.uuid + "/lldp_neighbors",
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
class PortCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of Port objects."""
|
||||||
|
|
||||||
|
ports = [Port]
|
||||||
|
"A list containing Port objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'ports'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_ports, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = PortCollection()
|
||||||
|
collection.ports = [Port.convert_with_links(p, expand)
|
||||||
|
for p in rpc_ports]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
class PortController(rest.RestController):
|
||||||
|
"""REST controller for Ports."""
|
||||||
|
|
||||||
|
lldp_agents = lldp_agent.LLDPAgentController(
|
||||||
|
from_ports=True)
|
||||||
|
"Expose lldp_agents as a sub-element of ports"
|
||||||
|
|
||||||
|
lldp_neighbours = lldp_neighbour.LLDPNeighbourController(
|
||||||
|
from_ports=True)
|
||||||
|
"Expose lldp_neighbours as a sub-element of ports"
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False, from_iinterface=False,
|
||||||
|
from_node=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._from_iinterface = from_iinterface
|
||||||
|
self._from_node = from_node
|
||||||
|
|
||||||
|
def _get_ports_collection(self, uuid, interface_uuid, node_uuid,
|
||||||
|
marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
if self._from_iinterface and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Interface id not specified."))
|
||||||
|
|
||||||
|
if self._from_node and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"node id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.Port.get_by_uuid(
|
||||||
|
pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
ports = objects.Port.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
elif self._from_node:
|
||||||
|
ports = objects.Port.get_by_numa_node(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
if uuid and not interface_uuid:
|
||||||
|
ports = objects.Port.get_by_host(
|
||||||
|
pecan.request.context,
|
||||||
|
uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
ports = objects.Port.list(
|
||||||
|
pecan.request.context,
|
||||||
|
limit, marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return PortCollection.convert_with_links(ports, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(PortCollection, types.uuid, types.uuid,
|
||||||
|
types.uuid, types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None, interface_uuid=None, node_uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of ports."""
|
||||||
|
|
||||||
|
return self._get_ports_collection(uuid,
|
||||||
|
interface_uuid,
|
||||||
|
node_uuid,
|
||||||
|
marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(PortCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of ports with detail."""
|
||||||
|
|
||||||
|
# NOTE(lucasagomes): /detail should only work against collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "ports":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['ports', 'detail'])
|
||||||
|
return self._get_ports_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(Port, types.uuid)
|
||||||
|
def get_one(self, port_uuid):
|
||||||
|
"""Retrieve information about the given port."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_port = objects.Port.get_by_uuid(
|
||||||
|
pecan.request.context, port_uuid)
|
||||||
|
return Port.convert_with_links(rpc_port)
|
|
@ -0,0 +1,168 @@
|
||||||
|
# coding: utf-8
|
||||||
|
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||||
|
# Copyright 2013 IBM Corp.
|
||||||
|
# Copyright © 2013 eNovance <licensing@enovance.com>
|
||||||
|
# Copyright Ericsson AB 2013. All rights reserved
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
import six
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
operation_kind = wtypes.Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
|
||||||
|
|
||||||
|
|
||||||
|
class _Base(wtypes.Base):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_db_model(cls, m):
|
||||||
|
return cls(**(m.as_dict()))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_db_and_links(cls, m, links):
|
||||||
|
return cls(links=links, **(m.as_dict()))
|
||||||
|
|
||||||
|
def as_dict(self, db_model):
|
||||||
|
valid_keys = inspect.getargspec(db_model.__init__)[0]
|
||||||
|
if 'self' in valid_keys:
|
||||||
|
valid_keys.remove('self')
|
||||||
|
return self.as_dict_from_keys(valid_keys)
|
||||||
|
|
||||||
|
def as_dict_from_keys(self, keys):
|
||||||
|
return dict((k, getattr(self, k))
|
||||||
|
for k in keys
|
||||||
|
if hasattr(self, k) and
|
||||||
|
getattr(self, k) != wsme.Unset)
|
||||||
|
|
||||||
|
|
||||||
|
class Query(_Base):
|
||||||
|
"""Query filter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The data types supported by the query.
|
||||||
|
_supported_types = ['integer', 'float', 'string', 'boolean']
|
||||||
|
|
||||||
|
# Functions to convert the data field to the correct type.
|
||||||
|
_type_converters = {'integer': int,
|
||||||
|
'float': float,
|
||||||
|
'boolean': functools.partial(
|
||||||
|
strutils.bool_from_string, strict=True),
|
||||||
|
'string': six.text_type,
|
||||||
|
'datetime': timeutils.parse_isotime}
|
||||||
|
|
||||||
|
_op = None # provide a default
|
||||||
|
|
||||||
|
def get_op(self):
|
||||||
|
return self._op or 'eq'
|
||||||
|
|
||||||
|
def set_op(self, value):
|
||||||
|
self._op = value
|
||||||
|
|
||||||
|
field = wtypes.text
|
||||||
|
"The name of the field to test"
|
||||||
|
|
||||||
|
# op = wsme.wsattr(operation_kind, default='eq')
|
||||||
|
# this ^ doesn't seem to work.
|
||||||
|
op = wsme.wsproperty(operation_kind, get_op, set_op)
|
||||||
|
"The comparison operator. Defaults to 'eq'."
|
||||||
|
|
||||||
|
value = wtypes.text
|
||||||
|
"The value to compare against the stored data"
|
||||||
|
|
||||||
|
type = wtypes.text
|
||||||
|
"The data type of value to compare against the stored data"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# for logging calls
|
||||||
|
return '<Query %r %s %r %s>' % (self.field,
|
||||||
|
self.op,
|
||||||
|
self.value,
|
||||||
|
self.type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
return cls(field='resource_id',
|
||||||
|
op='eq',
|
||||||
|
value='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
|
||||||
|
type='string'
|
||||||
|
)
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return self.as_dict_from_keys(['field', 'op', 'type', 'value'])
|
||||||
|
|
||||||
|
def _get_value_as_type(self, forced_type=None):
|
||||||
|
"""Convert metadata value to the specified data type.
|
||||||
|
|
||||||
|
This method is called during metadata query to help convert the
|
||||||
|
querying metadata to the data type specified by user. If there is no
|
||||||
|
data type given, the metadata will be parsed by ast.literal_eval to
|
||||||
|
try to do a smart converting.
|
||||||
|
|
||||||
|
NOTE (flwang) Using "_" as prefix to avoid an InvocationError raised
|
||||||
|
from wsmeext/sphinxext.py. It's OK to call it outside the Query class.
|
||||||
|
Because the "public" side of that class is actually the outside of the
|
||||||
|
API, and the "private" side is the API implementation. The method is
|
||||||
|
only used in the API implementation, so it's OK.
|
||||||
|
|
||||||
|
:returns: metadata value converted with the specified data type.
|
||||||
|
"""
|
||||||
|
type = forced_type or self.type
|
||||||
|
try:
|
||||||
|
converted_value = self.value
|
||||||
|
if not type:
|
||||||
|
try:
|
||||||
|
converted_value = ast.literal_eval(self.value)
|
||||||
|
except (ValueError, SyntaxError):
|
||||||
|
msg = _('Failed to convert the metadata value %s'
|
||||||
|
' automatically') % (self.value)
|
||||||
|
LOG.debug(msg)
|
||||||
|
else:
|
||||||
|
if type not in self._supported_types:
|
||||||
|
# Types must be explicitly declared so the
|
||||||
|
# correct type converter may be used. Subclasses
|
||||||
|
# of Query may define _supported_types and
|
||||||
|
# _type_converters to define their own types.
|
||||||
|
raise TypeError()
|
||||||
|
converted_value = self._type_converters[type](self.value)
|
||||||
|
except ValueError:
|
||||||
|
msg = _('Failed to convert the value %(value)s'
|
||||||
|
' to the expected data type %(type)s.') % \
|
||||||
|
{'value': self.value, 'type': type}
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
except TypeError:
|
||||||
|
msg = _('The data type %(type)s is not supported. The supported'
|
||||||
|
' data type list is: %(supported)s') % \
|
||||||
|
{'type': type, 'supported': self._supported_types}
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
except Exception:
|
||||||
|
msg = _('Unexpected exception converting %(value)s to'
|
||||||
|
' the expected data type %(type)s.') % \
|
||||||
|
{'value': self.value, 'type': type}
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
return converted_value
|
|
@ -0,0 +1,586 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import jsonpatch
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
import six
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import constants
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common import hwmon_api
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import k_host
|
||||||
|
from inventory.common import utils as cutils
|
||||||
|
from inventory import objects
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPatchType(types.JsonPatchType):
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(base.APIBase):
|
||||||
|
"""API representation of an Sensor
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
sensor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this sensor"
|
||||||
|
|
||||||
|
sensorname = wtypes.text
|
||||||
|
"Represent the name of the sensor. Unique with path per host"
|
||||||
|
|
||||||
|
path = wtypes.text
|
||||||
|
"Represent the path of the sensor. Unique with sensorname per host"
|
||||||
|
|
||||||
|
sensortype = wtypes.text
|
||||||
|
"Represent the type of sensor. e.g. Temperature, WatchDog"
|
||||||
|
|
||||||
|
datatype = wtypes.text
|
||||||
|
"Represent the entity monitored. e.g. discrete, analog"
|
||||||
|
|
||||||
|
status = wtypes.text
|
||||||
|
"Represent current sensor status: ok, minor, major, critical, disabled"
|
||||||
|
|
||||||
|
state = wtypes.text
|
||||||
|
"Represent the current state of the sensor"
|
||||||
|
|
||||||
|
state_requested = wtypes.text
|
||||||
|
"Represent the requested state of the sensor"
|
||||||
|
|
||||||
|
audit_interval = int
|
||||||
|
"Represent the audit_interval of the sensor."
|
||||||
|
|
||||||
|
algorithm = wtypes.text
|
||||||
|
"Represent the algorithm of the sensor."
|
||||||
|
|
||||||
|
actions_minor = wtypes.text
|
||||||
|
"Represent the minor configured actions of the sensor. CSV."
|
||||||
|
|
||||||
|
actions_major = wtypes.text
|
||||||
|
"Represent the major configured actions of the sensor. CSV."
|
||||||
|
|
||||||
|
actions_critical = wtypes.text
|
||||||
|
"Represent the critical configured actions of the sensor. CSV."
|
||||||
|
|
||||||
|
suppress = wtypes.text
|
||||||
|
"Represent supress sensor if True, otherwise not suppress sensor"
|
||||||
|
|
||||||
|
value = wtypes.text
|
||||||
|
"Represent current value of the discrete sensor"
|
||||||
|
|
||||||
|
unit_base = wtypes.text
|
||||||
|
"Represent the unit base of the analog sensor e.g. revolutions"
|
||||||
|
|
||||||
|
unit_modifier = wtypes.text
|
||||||
|
"Represent the unit modifier of the analog sensor e.g. 10**2"
|
||||||
|
|
||||||
|
unit_rate = wtypes.text
|
||||||
|
"Represent the unit rate of the sensor e.g. /minute"
|
||||||
|
|
||||||
|
t_minor_lower = wtypes.text
|
||||||
|
"Represent the minor lower threshold of the analog sensor"
|
||||||
|
|
||||||
|
t_minor_upper = wtypes.text
|
||||||
|
"Represent the minor upper threshold of the analog sensor"
|
||||||
|
|
||||||
|
t_major_lower = wtypes.text
|
||||||
|
"Represent the major lower threshold of the analog sensor"
|
||||||
|
|
||||||
|
t_major_upper = wtypes.text
|
||||||
|
"Represent the major upper threshold of the analog sensor"
|
||||||
|
|
||||||
|
t_critical_lower = wtypes.text
|
||||||
|
"Represent the critical lower threshold of the analog sensor"
|
||||||
|
|
||||||
|
t_critical_upper = wtypes.text
|
||||||
|
"Represent the critical upper threshold of the analog sensor"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
||||||
|
six.integer_types)}
|
||||||
|
"Represent meta data of the sensor"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"Represent the host_id the sensor belongs to"
|
||||||
|
|
||||||
|
sensorgroup_id = int
|
||||||
|
"Represent the sensorgroup_id the sensor belongs to"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"Represent the UUID of the host the sensor belongs to"
|
||||||
|
|
||||||
|
sensorgroup_uuid = types.uuid
|
||||||
|
"Represent the UUID of the sensorgroup the sensor belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated sensor links"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.Sensor.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_sensor, expand=True):
|
||||||
|
|
||||||
|
sensor = Sensor(**rpc_sensor.as_dict())
|
||||||
|
|
||||||
|
sensor_fields_common = ['uuid', 'host_id', 'sensorgroup_id',
|
||||||
|
'sensortype', 'datatype',
|
||||||
|
'sensorname', 'path',
|
||||||
|
|
||||||
|
'status',
|
||||||
|
'state', 'state_requested',
|
||||||
|
'sensor_action_requested',
|
||||||
|
'actions_minor',
|
||||||
|
'actions_major',
|
||||||
|
'actions_critical',
|
||||||
|
|
||||||
|
'suppress',
|
||||||
|
'audit_interval',
|
||||||
|
'algorithm',
|
||||||
|
'capabilities',
|
||||||
|
'host_uuid', 'sensorgroup_uuid',
|
||||||
|
'created_at', 'updated_at', ]
|
||||||
|
|
||||||
|
sensor_fields_analog = ['unit_base',
|
||||||
|
'unit_modifier',
|
||||||
|
'unit_rate',
|
||||||
|
|
||||||
|
't_minor_lower',
|
||||||
|
't_minor_upper',
|
||||||
|
't_major_lower',
|
||||||
|
't_major_upper',
|
||||||
|
't_critical_lower',
|
||||||
|
't_critical_upper', ]
|
||||||
|
|
||||||
|
if rpc_sensor.datatype == 'discrete':
|
||||||
|
sensor_fields = sensor_fields_common
|
||||||
|
elif rpc_sensor.datatype == 'analog':
|
||||||
|
sensor_fields = sensor_fields_common + sensor_fields_analog
|
||||||
|
else:
|
||||||
|
LOG.error(_("Invalid datatype={}").format(rpc_sensor.datatype))
|
||||||
|
|
||||||
|
if not expand:
|
||||||
|
sensor.unset_fields_except(sensor_fields)
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
sensor.host_id = wtypes.Unset
|
||||||
|
sensor.sensorgroup_id = wtypes.Unset
|
||||||
|
|
||||||
|
sensor.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'sensors', sensor.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensors', sensor.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
return sensor
|
||||||
|
|
||||||
|
|
||||||
|
class SensorCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of Sensor objects."""
|
||||||
|
|
||||||
|
sensors = [Sensor]
|
||||||
|
"A list containing Sensor objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'sensors'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_sensors, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = SensorCollection()
|
||||||
|
collection.sensors = [Sensor.convert_with_links(p, expand)
|
||||||
|
for p in rpc_sensors]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'SensorController'
|
||||||
|
|
||||||
|
|
||||||
|
class SensorController(rest.RestController):
|
||||||
|
"""REST controller for Sensors."""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False, from_sensorgroup=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._from_sensorgroup = from_sensorgroup
|
||||||
|
self._api_token = None
|
||||||
|
self._hwmon_address = k_host.LOCALHOST_HOSTNAME
|
||||||
|
self._hwmon_port = constants.HWMON_PORT
|
||||||
|
|
||||||
|
def _get_sensors_collection(self, uuid, sensorgroup_uuid,
|
||||||
|
marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
if self._from_sensorgroup and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"SensorGroup id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.Sensor.get_by_uuid(
|
||||||
|
pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
sensors = pecan.request.dbapi.sensor_get_by_host(
|
||||||
|
uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
LOG.debug("dbapi.sensor_get_by_host=%s" % sensors)
|
||||||
|
elif self._from_sensorgroup:
|
||||||
|
sensors = pecan.request.dbapi.sensor_get_by_sensorgroup(
|
||||||
|
uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
LOG.debug("dbapi.sensor_get_by_sensorgroup=%s" % sensors)
|
||||||
|
else:
|
||||||
|
if uuid and not sensorgroup_uuid:
|
||||||
|
sensors = pecan.request.dbapi.sensor_get_by_host(
|
||||||
|
uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
LOG.debug("dbapi.sensor_get_by_host=%s" % sensors)
|
||||||
|
elif uuid and sensorgroup_uuid: # Need ihost_uuid ?
|
||||||
|
sensors = pecan.request.dbapi.sensor_get_by_host_sensorgroup(
|
||||||
|
uuid,
|
||||||
|
sensorgroup_uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
LOG.debug("dbapi.sensor_get_by_host_sensorgroup=%s" %
|
||||||
|
sensors)
|
||||||
|
|
||||||
|
elif sensorgroup_uuid: # Need ihost_uuid ?
|
||||||
|
sensors = pecan.request.dbapi.sensor_get_by_host_sensorgroup(
|
||||||
|
uuid, # None
|
||||||
|
sensorgroup_uuid,
|
||||||
|
limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
else:
|
||||||
|
sensors = pecan.request.dbapi.sensor_get_list(
|
||||||
|
limit, marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return SensorCollection.convert_with_links(sensors, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(SensorCollection, types.uuid, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None, sensorgroup_uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of sensors."""
|
||||||
|
|
||||||
|
return self._get_sensors_collection(uuid, sensorgroup_uuid,
|
||||||
|
marker, limit,
|
||||||
|
sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(SensorCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of sensors with detail."""
|
||||||
|
|
||||||
|
# NOTE(lucasagomes): /detail should only work against collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "sensors":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['sensors', 'detail'])
|
||||||
|
return self._get_sensors_collection(uuid, marker, limit, sort_key,
|
||||||
|
sort_dir, expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(Sensor, types.uuid)
|
||||||
|
def get_one(self, sensor_uuid):
|
||||||
|
"""Retrieve information about the given sensor."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_sensor = objects.Sensor.get_by_uuid(
|
||||||
|
pecan.request.context, sensor_uuid)
|
||||||
|
|
||||||
|
if rpc_sensor.datatype == 'discrete':
|
||||||
|
rpc_sensor = objects.SensorDiscrete.get_by_uuid(
|
||||||
|
pecan.request.context, sensor_uuid)
|
||||||
|
elif rpc_sensor.datatype == 'analog':
|
||||||
|
rpc_sensor = objects.SensorAnalog.get_by_uuid(
|
||||||
|
pecan.request.context, sensor_uuid)
|
||||||
|
else:
|
||||||
|
LOG.error(_("Invalid datatype={}").format(rpc_sensor.datatype))
|
||||||
|
|
||||||
|
return Sensor.convert_with_links(rpc_sensor)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _new_sensor_semantic_checks(sensor):
|
||||||
|
datatype = sensor.as_dict().get('datatype') or ""
|
||||||
|
sensortype = sensor.as_dict().get('sensortype') or ""
|
||||||
|
if not (datatype and sensortype):
|
||||||
|
raise wsme.exc.ClientSideError(_("sensor-add Cannot "
|
||||||
|
"add a sensor "
|
||||||
|
"without a valid datatype "
|
||||||
|
"and sensortype."))
|
||||||
|
|
||||||
|
if datatype not in constants.SENSOR_DATATYPE_VALID_LIST:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("sensor datatype must be one of %s.") %
|
||||||
|
constants.SENSOR_DATATYPE_VALID_LIST)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(Sensor, body=Sensor)
|
||||||
|
def post(self, sensor):
|
||||||
|
"""Create a new sensor."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
self._new_sensor_semantic_checks(sensor)
|
||||||
|
try:
|
||||||
|
ihost = pecan.request.dbapi.host_get(sensor.host_uuid)
|
||||||
|
|
||||||
|
if hasattr(sensor, 'datatype'):
|
||||||
|
if sensor.datatype == 'discrete':
|
||||||
|
new_sensor = pecan.request.dbapi.sensor_discrete_create(
|
||||||
|
ihost.id, sensor.as_dict())
|
||||||
|
elif sensor.datatype == 'analog':
|
||||||
|
new_sensor = pecan.request.dbapi.sensor_analog_create(
|
||||||
|
ihost.id, sensor.as_dict())
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("Invalid datatype. {}").format(sensor.datatype))
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(_("Unspecified datatype."))
|
||||||
|
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid data"))
|
||||||
|
return sensor.convert_with_links(new_sensor)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme.validate(types.uuid, [SensorPatchType])
|
||||||
|
@wsme_pecan.wsexpose(Sensor, types.uuid,
|
||||||
|
body=[SensorPatchType])
|
||||||
|
def patch(self, sensor_uuid, patch):
|
||||||
|
"""Update an existing sensor."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rpc_sensor = objects.Sensor.get_by_uuid(pecan.request.context,
|
||||||
|
sensor_uuid)
|
||||||
|
if rpc_sensor.datatype == 'discrete':
|
||||||
|
rpc_sensor = objects.SensorDiscrete.get_by_uuid(
|
||||||
|
pecan.request.context, sensor_uuid)
|
||||||
|
elif rpc_sensor.datatype == 'analog':
|
||||||
|
rpc_sensor = objects.SensorAnalog.get_by_uuid(
|
||||||
|
pecan.request.context, sensor_uuid)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid datatype={}").format(
|
||||||
|
rpc_sensor.datatype))
|
||||||
|
|
||||||
|
rpc_sensor_orig = copy.deepcopy(rpc_sensor)
|
||||||
|
|
||||||
|
# replace ihost_uuid and sensorgroup_uuid with corresponding
|
||||||
|
utils.validate_patch(patch)
|
||||||
|
patch_obj = jsonpatch.JsonPatch(patch)
|
||||||
|
my_host_uuid = None
|
||||||
|
for p in patch_obj:
|
||||||
|
if p['path'] == '/host_uuid':
|
||||||
|
p['path'] = '/host_id'
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
p['value'])
|
||||||
|
p['value'] = host.id
|
||||||
|
my_host_uuid = host.uuid
|
||||||
|
|
||||||
|
if p['path'] == '/sensorgroup_uuid':
|
||||||
|
p['path'] = '/sensorgroup_id'
|
||||||
|
try:
|
||||||
|
sensorgroup = objects.sensorgroup.get_by_uuid(
|
||||||
|
pecan.request.context, p['value'])
|
||||||
|
p['value'] = sensorgroup.id
|
||||||
|
LOG.info("sensorgroup_uuid=%s id=%s" % (p['value'],
|
||||||
|
sensorgroup.id))
|
||||||
|
except exception.InventoryException:
|
||||||
|
p['value'] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
sensor = Sensor(**jsonpatch.apply_patch(rpc_sensor.as_dict(),
|
||||||
|
patch_obj))
|
||||||
|
|
||||||
|
except utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Update only the fields that have changed
|
||||||
|
if rpc_sensor.datatype == 'discrete':
|
||||||
|
fields = objects.SensorDiscrete.fields
|
||||||
|
else:
|
||||||
|
fields = objects.SensorAnalog.fields
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
if rpc_sensor[field] != getattr(sensor, field):
|
||||||
|
rpc_sensor[field] = getattr(sensor, field)
|
||||||
|
|
||||||
|
delta = rpc_sensor.obj_what_changed()
|
||||||
|
sensor_suppress_attrs = ['suppress']
|
||||||
|
force_action = False
|
||||||
|
if any(x in delta for x in sensor_suppress_attrs):
|
||||||
|
valid_suppress = ['True', 'False', 'true', 'false', 'force_action']
|
||||||
|
if rpc_sensor.suppress.lower() not in valid_suppress:
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid suppress value, "
|
||||||
|
"select 'True' or 'False'"))
|
||||||
|
elif rpc_sensor.suppress.lower() == 'force_action':
|
||||||
|
LOG.info("suppress=%s" % rpc_sensor.suppress.lower())
|
||||||
|
rpc_sensor.suppress = rpc_sensor_orig.suppress
|
||||||
|
force_action = True
|
||||||
|
|
||||||
|
self._semantic_modifiable_fields(patch_obj, force_action)
|
||||||
|
|
||||||
|
if not pecan.request.user_agent.startswith('hwmon'):
|
||||||
|
hwmon_sensor = cutils.removekeys_nonhwmon(
|
||||||
|
rpc_sensor.as_dict())
|
||||||
|
|
||||||
|
if not my_host_uuid:
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
rpc_sensor.host_id)
|
||||||
|
my_host_uuid = host.uuid
|
||||||
|
LOG.warn("Missing host_uuid updated=%s" % my_host_uuid)
|
||||||
|
|
||||||
|
hwmon_sensor.update({'host_uuid': my_host_uuid})
|
||||||
|
|
||||||
|
hwmon_response = hwmon_api.sensor_modify(
|
||||||
|
self._api_token, self._hwmon_address, self._hwmon_port,
|
||||||
|
hwmon_sensor,
|
||||||
|
constants.HWMON_DEFAULT_TIMEOUT_IN_SECS)
|
||||||
|
|
||||||
|
if not hwmon_response:
|
||||||
|
hwmon_response = {'status': 'fail',
|
||||||
|
'reason': 'no response',
|
||||||
|
'action': 'retry'}
|
||||||
|
|
||||||
|
if hwmon_response['status'] != 'pass':
|
||||||
|
msg = _("HWMON has returned with a status of {}, reason: {}, "
|
||||||
|
"recommended action: {}").format(
|
||||||
|
hwmon_response.get('status'),
|
||||||
|
hwmon_response.get('reason'),
|
||||||
|
hwmon_response.get('action'))
|
||||||
|
|
||||||
|
if force_action:
|
||||||
|
LOG.error(msg)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
rpc_sensor.save()
|
||||||
|
|
||||||
|
return Sensor.convert_with_links(rpc_sensor)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
|
def delete(self, sensor_uuid):
|
||||||
|
"""Delete a sensor."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
pecan.request.dbapi.sensor_destroy(sensor_uuid)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _semantic_modifiable_fields(patch_obj, force_action=False):
|
||||||
|
# Prevent auto populated fields from being updated
|
||||||
|
state_rel_path = ['/uuid', '/id', '/host_id', '/datatype',
|
||||||
|
'/sensortype']
|
||||||
|
if any(p['path'] in state_rel_path for p in patch_obj):
|
||||||
|
raise wsme.exc.ClientSideError(_("The following fields can not be "
|
||||||
|
"modified: %s ") % state_rel_path)
|
||||||
|
|
||||||
|
state_rel_path = ['/actions_critical',
|
||||||
|
'/actions_major',
|
||||||
|
'/actions_minor']
|
||||||
|
if any(p['path'] in state_rel_path for p in patch_obj):
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("The following fields can only be modified at the "
|
||||||
|
"sensorgroup level: %s") % state_rel_path)
|
||||||
|
|
||||||
|
if not (pecan.request.user_agent.startswith('hwmon') or force_action):
|
||||||
|
state_rel_path = ['/sensorname',
|
||||||
|
'/path',
|
||||||
|
'/status',
|
||||||
|
'/state',
|
||||||
|
'/possible_states',
|
||||||
|
'/algorithm',
|
||||||
|
'/actions_critical_choices',
|
||||||
|
'/actions_major_choices',
|
||||||
|
'/actions_minor_choices',
|
||||||
|
'/unit_base',
|
||||||
|
'/unit_modifier',
|
||||||
|
'/unit_rate',
|
||||||
|
'/t_minor_lower',
|
||||||
|
'/t_minor_upper',
|
||||||
|
'/t_major_lower',
|
||||||
|
'/t_major_upper',
|
||||||
|
'/t_critical_lower',
|
||||||
|
'/t_critical_upper',
|
||||||
|
]
|
||||||
|
|
||||||
|
if any(p['path'] in state_rel_path for p in patch_obj):
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("The following fields are not remote-modifiable: %s") %
|
||||||
|
state_rel_path)
|
|
@ -0,0 +1,751 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 UnitedStack Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import jsonpatch
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
import six
|
||||||
|
import uuid
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import sensor as sensor_api
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils
|
||||||
|
from inventory.common import constants
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common import hwmon_api
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import k_host
|
||||||
|
from inventory.common import utils as cutils
|
||||||
|
from inventory import objects
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
from six import text_type as unicode
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SensorGroupPatchType(types.JsonPatchType):
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
return ['/host_uuid', 'uuid']
|
||||||
|
|
||||||
|
|
||||||
|
class SensorGroup(base.APIBase):
|
||||||
|
"""API representation of an Sensor Group
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of an
|
||||||
|
sensorgroup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"Unique UUID for this sensorgroup"
|
||||||
|
|
||||||
|
sensorgroupname = wtypes.text
|
||||||
|
"Represent the name of the sensorgroup. Unique with path per host"
|
||||||
|
|
||||||
|
path = wtypes.text
|
||||||
|
"Represent the path of the sensor. Unique with sensorname per host"
|
||||||
|
|
||||||
|
sensortype = wtypes.text
|
||||||
|
"Represent the sensortype . e.g. Temperature, WatchDog"
|
||||||
|
|
||||||
|
datatype = wtypes.text
|
||||||
|
"Represent the datatype e.g. discrete or analog,"
|
||||||
|
|
||||||
|
state = wtypes.text
|
||||||
|
"Represent the state of the sensorgroup"
|
||||||
|
|
||||||
|
possible_states = wtypes.text
|
||||||
|
"Represent the possible states of the sensorgroup"
|
||||||
|
|
||||||
|
algorithm = wtypes.text
|
||||||
|
"Represent the algorithm of the sensorgroup."
|
||||||
|
|
||||||
|
audit_interval_group = int
|
||||||
|
"Represent the audit interval of the sensorgroup."
|
||||||
|
|
||||||
|
actions_critical_choices = wtypes.text
|
||||||
|
"Represent the configurable critical severity actions of the sensorgroup."
|
||||||
|
|
||||||
|
actions_major_choices = wtypes.text
|
||||||
|
"Represent the configurable major severity actions of the sensorgroup."
|
||||||
|
|
||||||
|
actions_minor_choices = wtypes.text
|
||||||
|
"Represent the configurable minor severity actions of the sensorgroup."
|
||||||
|
|
||||||
|
actions_minor_group = wtypes.text
|
||||||
|
"Represent the minor configured actions of the sensorgroup. CSV."
|
||||||
|
|
||||||
|
actions_major_group = wtypes.text
|
||||||
|
"Represent the major configured actions of the sensorgroup. CSV."
|
||||||
|
|
||||||
|
actions_critical_group = wtypes.text
|
||||||
|
"Represent the critical configured actions of the sensorgroup. CSV."
|
||||||
|
|
||||||
|
unit_base_group = wtypes.text
|
||||||
|
"Represent the unit base of the analog sensorgroup e.g. revolutions"
|
||||||
|
|
||||||
|
unit_modifier_group = wtypes.text
|
||||||
|
"Represent the unit modifier of the analog sensorgroup e.g. 10**2"
|
||||||
|
|
||||||
|
unit_rate_group = wtypes.text
|
||||||
|
"Represent the unit rate of the sensorgroup e.g. /minute"
|
||||||
|
|
||||||
|
t_minor_lower_group = wtypes.text
|
||||||
|
"Represent the minor lower threshold of the analog sensorgroup"
|
||||||
|
|
||||||
|
t_minor_upper_group = wtypes.text
|
||||||
|
"Represent the minor upper threshold of the analog sensorgroup"
|
||||||
|
|
||||||
|
t_major_lower_group = wtypes.text
|
||||||
|
"Represent the major lower threshold of the analog sensorgroup"
|
||||||
|
|
||||||
|
t_major_upper_group = wtypes.text
|
||||||
|
"Represent the major upper threshold of the analog sensorgroup"
|
||||||
|
|
||||||
|
t_critical_lower_group = wtypes.text
|
||||||
|
"Represent the critical lower threshold of the analog sensorgroup"
|
||||||
|
|
||||||
|
t_critical_upper_group = wtypes.text
|
||||||
|
"Represent the critical upper threshold of the analog sensorgroup"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
||||||
|
six.integer_types)}
|
||||||
|
"Represent meta data of the sensorgroup"
|
||||||
|
|
||||||
|
suppress = wtypes.text
|
||||||
|
"Represent supress sensor if True, otherwise not suppress sensor"
|
||||||
|
|
||||||
|
sensors = wtypes.text
|
||||||
|
"Represent the sensors of the sensorgroup"
|
||||||
|
|
||||||
|
host_id = int
|
||||||
|
"Represent the host_id the sensorgroup belongs to"
|
||||||
|
|
||||||
|
host_uuid = types.uuid
|
||||||
|
"Represent the UUID of the host the sensorgroup belongs to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"Represent a list containing a self link and associated sensorgroup links"
|
||||||
|
|
||||||
|
sensors = [link.Link]
|
||||||
|
"Links to the collection of sensors on this sensorgroup"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.SensorGroup.fields.keys()
|
||||||
|
for k in self.fields:
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
# 'sensors' is not part of objects.SenorGroups.fields (it's an
|
||||||
|
# API-only attribute)
|
||||||
|
self.fields.append('sensors')
|
||||||
|
setattr(self, 'sensors', kwargs.get('sensors', None))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rsensorgroup, expand=True):
|
||||||
|
|
||||||
|
sensorgroup = SensorGroup(**rsensorgroup.as_dict())
|
||||||
|
|
||||||
|
sensorgroup_fields_common = ['uuid', 'host_id',
|
||||||
|
'host_uuid',
|
||||||
|
'sensortype', 'datatype',
|
||||||
|
'sensorgroupname',
|
||||||
|
'path',
|
||||||
|
|
||||||
|
'state',
|
||||||
|
'possible_states',
|
||||||
|
'audit_interval_group',
|
||||||
|
'algorithm',
|
||||||
|
'actions_critical_choices',
|
||||||
|
'actions_major_choices',
|
||||||
|
'actions_minor_choices',
|
||||||
|
'actions_minor_group',
|
||||||
|
'actions_major_group',
|
||||||
|
'actions_critical_group',
|
||||||
|
'sensors',
|
||||||
|
|
||||||
|
'suppress',
|
||||||
|
'capabilities',
|
||||||
|
'created_at', 'updated_at', ]
|
||||||
|
|
||||||
|
sensorgroup_fields_analog = ['unit_base_group',
|
||||||
|
'unit_modifier_group',
|
||||||
|
'unit_rate_group',
|
||||||
|
|
||||||
|
't_minor_lower_group',
|
||||||
|
't_minor_upper_group',
|
||||||
|
't_major_lower_group',
|
||||||
|
't_major_upper_group',
|
||||||
|
't_critical_lower_group',
|
||||||
|
't_critical_upper_group', ]
|
||||||
|
|
||||||
|
if rsensorgroup.datatype == 'discrete':
|
||||||
|
sensorgroup_fields = sensorgroup_fields_common
|
||||||
|
elif rsensorgroup.datatype == 'analog':
|
||||||
|
sensorgroup_fields = \
|
||||||
|
sensorgroup_fields_common + sensorgroup_fields_analog
|
||||||
|
else:
|
||||||
|
LOG.error(_("Invalid datatype={}").format(rsensorgroup.datatype))
|
||||||
|
|
||||||
|
if not expand:
|
||||||
|
sensorgroup.unset_fields_except(sensorgroup_fields)
|
||||||
|
|
||||||
|
if sensorgroup.host_id and not sensorgroup.host_uuid:
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
sensorgroup.host_id)
|
||||||
|
sensorgroup.host_uuid = host.uuid
|
||||||
|
|
||||||
|
# never expose the id attribute
|
||||||
|
sensorgroup.host_id = wtypes.Unset
|
||||||
|
sensorgroup.id = wtypes.Unset
|
||||||
|
|
||||||
|
sensorgroup.links = [
|
||||||
|
link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'sensorgroups',
|
||||||
|
sensorgroup.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensorgroups',
|
||||||
|
sensorgroup.uuid,
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
|
sensorgroup.sensors = [
|
||||||
|
link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensorgroups',
|
||||||
|
sensorgroup.uuid + "/sensors"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'sensorgroups',
|
||||||
|
sensorgroup.uuid + "/sensors",
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
|
return sensorgroup
|
||||||
|
|
||||||
|
|
||||||
|
class SensorGroupCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of SensorGroup objects."""
|
||||||
|
|
||||||
|
sensorgroups = [SensorGroup]
|
||||||
|
"A list containing SensorGroup objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'sensorgroups'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rsensorgroups, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = SensorGroupCollection()
|
||||||
|
collection.sensorgroups = [SensorGroup.convert_with_links(p, expand)
|
||||||
|
for p in rsensorgroups]
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'SensorGroupController'
|
||||||
|
|
||||||
|
|
||||||
|
class SensorGroupController(rest.RestController):
|
||||||
|
"""REST controller for SensorGroups."""
|
||||||
|
|
||||||
|
sensors = sensor_api.SensorController(from_sensorgroup=True)
|
||||||
|
"Expose sensors as a sub-element of sensorgroups"
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
'relearn': ['POST'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, from_hosts=False):
|
||||||
|
self._from_hosts = from_hosts
|
||||||
|
self._api_token = None
|
||||||
|
self._hwmon_address = k_host.LOCALHOST_HOSTNAME
|
||||||
|
self._hwmon_port = constants.HWMON_PORT
|
||||||
|
|
||||||
|
def _get_sensorgroups_collection(self, uuid,
|
||||||
|
marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
|
||||||
|
if self._from_hosts and not uuid:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"Host id not specified."))
|
||||||
|
|
||||||
|
limit = utils.validate_limit(limit)
|
||||||
|
sort_dir = utils.validate_sort_dir(sort_dir)
|
||||||
|
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.SensorGroup.get_by_uuid(
|
||||||
|
pecan.request.context,
|
||||||
|
marker)
|
||||||
|
|
||||||
|
if self._from_hosts:
|
||||||
|
sensorgroups = pecan.request.dbapi.sensorgroup_get_by_host(
|
||||||
|
uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
if uuid:
|
||||||
|
sensorgroups = pecan.request.dbapi.sensorgroup_get_by_host(
|
||||||
|
uuid, limit,
|
||||||
|
marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
else:
|
||||||
|
sensorgroups = pecan.request.dbapi.sensorgroup_get_list(
|
||||||
|
limit, marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
return SensorGroupCollection.convert_with_links(sensorgroups, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(SensorGroupCollection, types.uuid,
|
||||||
|
types.uuid, int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, uuid=None,
|
||||||
|
marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of sensorgroups."""
|
||||||
|
|
||||||
|
return self._get_sensorgroups_collection(uuid,
|
||||||
|
marker, limit,
|
||||||
|
sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(SensorGroupCollection, types.uuid, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, uuid=None, marker=None, limit=None,
|
||||||
|
sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of sensorgroups with detail."""
|
||||||
|
|
||||||
|
# NOTE(lucasagomes): /detail should only work against collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "sensorgroups":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['sensorgroups', 'detail'])
|
||||||
|
return self._get_sensorgroups_collection(uuid, marker, limit,
|
||||||
|
sort_key, sort_dir,
|
||||||
|
expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(SensorGroup, types.uuid)
|
||||||
|
def get_one(self, sensorgroup_uuid):
|
||||||
|
"""Retrieve information about the given sensorgroup."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rsensorgroup = objects.SensorGroup.get_by_uuid(
|
||||||
|
pecan.request.context, sensorgroup_uuid)
|
||||||
|
|
||||||
|
if rsensorgroup.datatype == 'discrete':
|
||||||
|
rsensorgroup = objects.SensorGroupDiscrete.get_by_uuid(
|
||||||
|
pecan.request.context, sensorgroup_uuid)
|
||||||
|
elif rsensorgroup.datatype == 'analog':
|
||||||
|
rsensorgroup = objects.SensorGroupAnalog.get_by_uuid(
|
||||||
|
pecan.request.context, sensorgroup_uuid)
|
||||||
|
else:
|
||||||
|
LOG.error(_("Invalid datatype={}").format(rsensorgroup.datatype))
|
||||||
|
|
||||||
|
return SensorGroup.convert_with_links(rsensorgroup)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _new_sensorgroup_semantic_checks(sensorgroup):
|
||||||
|
datatype = sensorgroup.as_dict().get('datatype') or ""
|
||||||
|
sensortype = sensorgroup.as_dict().get('sensortype') or ""
|
||||||
|
if not (datatype and sensortype):
|
||||||
|
raise wsme.exc.ClientSideError(_("sensorgroup-add: Cannot "
|
||||||
|
"add a sensorgroup "
|
||||||
|
"without a valid datatype "
|
||||||
|
"and sensortype."))
|
||||||
|
|
||||||
|
if datatype not in constants.SENSOR_DATATYPE_VALID_LIST:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("sensorgroup datatype must be one of %s.") %
|
||||||
|
constants.SENSOR_DATATYPE_VALID_LIST)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(SensorGroup, body=SensorGroup)
|
||||||
|
def post(self, sensorgroup):
|
||||||
|
"""Create a new sensorgroup."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
self._new_sensorgroup_semantic_checks(sensorgroup)
|
||||||
|
try:
|
||||||
|
sensorgroup_dict = sensorgroup.as_dict()
|
||||||
|
new_sensorgroup = _create(sensorgroup_dict)
|
||||||
|
|
||||||
|
except exception.InventoryException as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid data"))
|
||||||
|
return sensorgroup.convert_with_links(new_sensorgroup)
|
||||||
|
|
||||||
|
def _get_host_uuid(self, body):
|
||||||
|
host_uuid = body.get('host_uuid') or ""
|
||||||
|
try:
|
||||||
|
host = pecan.request.dbapi.host_get(host_uuid)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise wsme.exc.ClientSideError("_get_host_uuid lookup failed")
|
||||||
|
return host.uuid
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose('json', body=unicode)
|
||||||
|
def relearn(self, body):
|
||||||
|
"""Handle Sensor Model Relearn Request."""
|
||||||
|
host_uuid = self._get_host_uuid(body)
|
||||||
|
# LOG.info("Host UUID: %s - BM_TYPE: %s" % (host_uuid, bm_type ))
|
||||||
|
|
||||||
|
# hwmon_sensorgroup = {'ihost_uuid': host_uuid}
|
||||||
|
request_body = {'host_uuid': host_uuid}
|
||||||
|
hwmon_response = hwmon_api.sensorgroup_relearn(
|
||||||
|
self._api_token, self._hwmon_address, self._hwmon_port,
|
||||||
|
request_body,
|
||||||
|
constants.HWMON_DEFAULT_TIMEOUT_IN_SECS)
|
||||||
|
|
||||||
|
if not hwmon_response:
|
||||||
|
hwmon_response = {'status': 'fail',
|
||||||
|
'reason': 'no response',
|
||||||
|
'action': 'retry'}
|
||||||
|
|
||||||
|
elif hwmon_response['status'] != 'pass':
|
||||||
|
msg = _("HWMON has returned with "
|
||||||
|
"a status of {}, reason: {}, "
|
||||||
|
"recommended action: {}").format(
|
||||||
|
hwmon_response.get('status'),
|
||||||
|
hwmon_response.get('reason'),
|
||||||
|
hwmon_response.get('action'))
|
||||||
|
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme.validate(types.uuid, [SensorGroupPatchType])
|
||||||
|
@wsme_pecan.wsexpose(SensorGroup, types.uuid,
|
||||||
|
body=[SensorGroupPatchType])
|
||||||
|
def patch(self, sensorgroup_uuid, patch):
|
||||||
|
"""Update an existing sensorgroup."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
rsensorgroup = objects.SensorGroup.get_by_uuid(
|
||||||
|
pecan.request.context, sensorgroup_uuid)
|
||||||
|
|
||||||
|
if rsensorgroup.datatype == 'discrete':
|
||||||
|
rsensorgroup = objects.SensorGroupDiscrete.get_by_uuid(
|
||||||
|
pecan.request.context, sensorgroup_uuid)
|
||||||
|
elif rsensorgroup.datatype == 'analog':
|
||||||
|
rsensorgroup = objects.SensorGroupAnalog.get_by_uuid(
|
||||||
|
pecan.request.context, sensorgroup_uuid)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid datatype={}").format(
|
||||||
|
rsensorgroup.datatype))
|
||||||
|
|
||||||
|
rsensorgroup_orig = copy.deepcopy(rsensorgroup)
|
||||||
|
|
||||||
|
host = pecan.request.dbapi.host_get(
|
||||||
|
rsensorgroup['host_id']).as_dict()
|
||||||
|
|
||||||
|
utils.validate_patch(patch)
|
||||||
|
patch_obj = jsonpatch.JsonPatch(patch)
|
||||||
|
my_host_uuid = None
|
||||||
|
for p in patch_obj:
|
||||||
|
# For Profile replace host_uuid with corresponding id
|
||||||
|
if p['path'] == '/host_uuid':
|
||||||
|
p['path'] = '/host_id'
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
p['value'])
|
||||||
|
p['value'] = host.id
|
||||||
|
my_host_uuid = host.uuid
|
||||||
|
|
||||||
|
# update sensors if set
|
||||||
|
sensors = None
|
||||||
|
for s in patch:
|
||||||
|
if '/sensors' in s['path']:
|
||||||
|
sensors = s['value']
|
||||||
|
patch.remove(s)
|
||||||
|
break
|
||||||
|
|
||||||
|
if sensors:
|
||||||
|
_update_sensors("modify", rsensorgroup, host, sensors)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sensorgroup = SensorGroup(**jsonpatch.apply_patch(
|
||||||
|
rsensorgroup.as_dict(),
|
||||||
|
patch_obj))
|
||||||
|
|
||||||
|
except utils.JSONPATCH_EXCEPTIONS as e:
|
||||||
|
raise exception.PatchError(patch=patch, reason=e)
|
||||||
|
|
||||||
|
# Update only the fields that have changed
|
||||||
|
if rsensorgroup.datatype == 'discrete':
|
||||||
|
fields = objects.SensorGroupDiscrete.fields
|
||||||
|
else:
|
||||||
|
fields = objects.SensorGroupAnalog.fields
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
if rsensorgroup[field] != getattr(sensorgroup, field):
|
||||||
|
rsensorgroup[field] = getattr(sensorgroup, field)
|
||||||
|
|
||||||
|
delta = rsensorgroup.obj_what_changed()
|
||||||
|
|
||||||
|
sensorgroup_suppress_attrs = ['suppress']
|
||||||
|
force_action = False
|
||||||
|
if any(x in delta for x in sensorgroup_suppress_attrs):
|
||||||
|
valid_suppress = ['True', 'False', 'true', 'false', 'force_action']
|
||||||
|
if rsensorgroup.suppress.lower() not in valid_suppress:
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid suppress value, "
|
||||||
|
"select 'True' or 'False'"))
|
||||||
|
elif rsensorgroup.suppress.lower() == 'force_action':
|
||||||
|
LOG.info("suppress=%s" % rsensorgroup.suppress.lower())
|
||||||
|
rsensorgroup.suppress = rsensorgroup_orig.suppress
|
||||||
|
force_action = True
|
||||||
|
|
||||||
|
self._semantic_modifiable_fields(patch_obj, force_action)
|
||||||
|
|
||||||
|
if not pecan.request.user_agent.startswith('hwmon'):
|
||||||
|
hwmon_sensorgroup = cutils.removekeys_nonhwmon(
|
||||||
|
rsensorgroup.as_dict())
|
||||||
|
|
||||||
|
if not my_host_uuid:
|
||||||
|
host = objects.Host.get_by_uuid(pecan.request.context,
|
||||||
|
rsensorgroup.host_id)
|
||||||
|
my_host_uuid = host.uuid
|
||||||
|
|
||||||
|
hwmon_sensorgroup.update({'host_uuid': my_host_uuid})
|
||||||
|
|
||||||
|
hwmon_response = hwmon_api.sensorgroup_modify(
|
||||||
|
self._api_token, self._hwmon_address, self._hwmon_port,
|
||||||
|
hwmon_sensorgroup,
|
||||||
|
constants.HWMON_DEFAULT_TIMEOUT_IN_SECS)
|
||||||
|
|
||||||
|
if not hwmon_response:
|
||||||
|
hwmon_response = {'status': 'fail',
|
||||||
|
'reason': 'no response',
|
||||||
|
'action': 'retry'}
|
||||||
|
|
||||||
|
if hwmon_response['status'] != 'pass':
|
||||||
|
msg = _("HWMON has returned with a status of {}, reason: {}, "
|
||||||
|
"recommended action: {}").format(
|
||||||
|
hwmon_response.get('status'),
|
||||||
|
hwmon_response.get('reason'),
|
||||||
|
hwmon_response.get('action'))
|
||||||
|
|
||||||
|
if force_action:
|
||||||
|
LOG.error(msg)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
sensorgroup_prop_attrs = ['audit_interval_group',
|
||||||
|
'actions_minor_group',
|
||||||
|
'actions_major_group',
|
||||||
|
'actions_critical_group',
|
||||||
|
'suppress']
|
||||||
|
|
||||||
|
if any(x in delta for x in sensorgroup_prop_attrs):
|
||||||
|
# propagate to Sensors within this SensorGroup
|
||||||
|
sensor_val = {'audit_interval': rsensorgroup.audit_interval_group,
|
||||||
|
'actions_minor': rsensorgroup.actions_minor_group,
|
||||||
|
'actions_major': rsensorgroup.actions_major_group,
|
||||||
|
'actions_critical':
|
||||||
|
rsensorgroup.actions_critical_group}
|
||||||
|
if 'suppress' in delta:
|
||||||
|
sensor_val.update({'suppress': rsensorgroup.suppress})
|
||||||
|
pecan.request.dbapi.sensorgroup_propagate(
|
||||||
|
rsensorgroup.uuid, sensor_val)
|
||||||
|
|
||||||
|
rsensorgroup.save()
|
||||||
|
|
||||||
|
return SensorGroup.convert_with_links(rsensorgroup)
|
||||||
|
|
||||||
|
@cutils.synchronized(LOCK_NAME)
|
||||||
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
|
def delete(self, sensorgroup_uuid):
|
||||||
|
"""Delete a sensorgroup."""
|
||||||
|
if self._from_hosts:
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
pecan.request.dbapi.sensorgroup_destroy(sensorgroup_uuid)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _semantic_modifiable_fields(patch_obj, force_action=False):
|
||||||
|
# Prevent auto populated fields from being updated
|
||||||
|
state_rel_path = ['/uuid', '/id', '/host_id', '/datatype',
|
||||||
|
'/sensortype']
|
||||||
|
|
||||||
|
if any(p['path'] in state_rel_path for p in patch_obj):
|
||||||
|
raise wsme.exc.ClientSideError(_("The following fields can not be "
|
||||||
|
"modified: %s ") % state_rel_path)
|
||||||
|
|
||||||
|
if not (pecan.request.user_agent.startswith('hwmon') or force_action):
|
||||||
|
state_rel_path = ['/sensorgroupname', '/path',
|
||||||
|
'/state', '/possible_states',
|
||||||
|
'/actions_critical_choices',
|
||||||
|
'/actions_major_choices',
|
||||||
|
'/actions_minor_choices',
|
||||||
|
'/unit_base_group',
|
||||||
|
'/unit_modifier_group',
|
||||||
|
'/unit_rate_group',
|
||||||
|
'/t_minor_lower_group',
|
||||||
|
'/t_minor_upper_group',
|
||||||
|
'/t_major_lower_group',
|
||||||
|
'/t_major_upper_group',
|
||||||
|
'/t_critical_lower_group',
|
||||||
|
'/t_critical_upper_group',
|
||||||
|
]
|
||||||
|
|
||||||
|
if any(p['path'] in state_rel_path for p in patch_obj):
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("The following fields are not remote-modifiable: %s") %
|
||||||
|
state_rel_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _create(sensorgroup, from_profile=False):
|
||||||
|
"""Create a sensorgroup through a non-HTTP request e.g. via profile.py
|
||||||
|
while still passing through sensorgroup semantic checks.
|
||||||
|
Hence, not declared inside a class.
|
||||||
|
Param:
|
||||||
|
sensorgroup - dictionary of sensorgroup values
|
||||||
|
from_profile - Boolean whether from profile
|
||||||
|
"""
|
||||||
|
|
||||||
|
if 'host_id' in sensorgroup and sensorgroup['host_id']:
|
||||||
|
ihostid = sensorgroup['host_id']
|
||||||
|
else:
|
||||||
|
ihostid = sensorgroup['host_uuid']
|
||||||
|
|
||||||
|
ihost = pecan.request.dbapi.host_get(ihostid)
|
||||||
|
if uuidutils.is_uuid_like(ihostid):
|
||||||
|
host_id = ihost['id']
|
||||||
|
else:
|
||||||
|
host_id = ihostid
|
||||||
|
sensorgroup.update({'host_id': host_id})
|
||||||
|
LOG.info("sensorgroup post sensorgroups ihostid: %s" % host_id)
|
||||||
|
sensorgroup['host_uuid'] = ihost['uuid']
|
||||||
|
|
||||||
|
# Assign UUID if not already done.
|
||||||
|
if not sensorgroup.get('uuid'):
|
||||||
|
sensorgroup['uuid'] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Get sensors
|
||||||
|
sensors = None
|
||||||
|
if 'sensors' in sensorgroup:
|
||||||
|
sensors = sensorgroup['sensors']
|
||||||
|
|
||||||
|
# Set defaults - before checks to allow for optional attributes
|
||||||
|
# if not from_profile:
|
||||||
|
# sensorgroup = _set_defaults(sensorgroup)
|
||||||
|
|
||||||
|
# Semantic checks
|
||||||
|
# sensorgroup = _check("add",
|
||||||
|
# sensorgroup,
|
||||||
|
# sensors=sensors,
|
||||||
|
# ifaces=uses_if,
|
||||||
|
# from_profile=from_profile)
|
||||||
|
|
||||||
|
if sensorgroup.get('datatype'):
|
||||||
|
if sensorgroup['datatype'] == 'discrete':
|
||||||
|
new_sensorgroup = pecan.request.dbapi.sensorgroup_discrete_create(
|
||||||
|
ihost.id, sensorgroup)
|
||||||
|
elif sensorgroup['datatype'] == 'analog':
|
||||||
|
new_sensorgroup = pecan.request.dbapi.sensorgroup_analog_create(
|
||||||
|
ihost.id, sensorgroup)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid datatype. %s") %
|
||||||
|
sensorgroup.datatype)
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(_("Unspecified datatype."))
|
||||||
|
|
||||||
|
# Update sensors
|
||||||
|
if sensors:
|
||||||
|
try:
|
||||||
|
_update_sensors("modify",
|
||||||
|
new_sensorgroup.as_dict(),
|
||||||
|
ihost,
|
||||||
|
sensors)
|
||||||
|
except Exception as e:
|
||||||
|
pecan.request.dbapi.sensorgroup_destroy(
|
||||||
|
new_sensorgroup.as_dict()['uuid'])
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Update sensors
|
||||||
|
# return new_sensorgroup
|
||||||
|
return SensorGroup.convert_with_links(new_sensorgroup)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_sensors(op, sensorgroup, ihost, sensors):
|
||||||
|
sensors = sensors.split(',')
|
||||||
|
|
||||||
|
this_sensorgroup_datatype = None
|
||||||
|
this_sensorgroup_sensortype = None
|
||||||
|
if op == "add":
|
||||||
|
this_sensorgroup_id = 0
|
||||||
|
else:
|
||||||
|
this_sensorgroup_id = sensorgroup['id']
|
||||||
|
this_sensorgroup_datatype = sensorgroup['datatype']
|
||||||
|
this_sensorgroup_sensortype = sensorgroup['sensortype']
|
||||||
|
|
||||||
|
if sensors:
|
||||||
|
# Update Sensors' sensorgroup_uuid attribute
|
||||||
|
sensors_list = pecan.request.dbapi.sensor_get_all(
|
||||||
|
host_id=ihost['id'])
|
||||||
|
for p in sensors_list:
|
||||||
|
# if new sensor associated
|
||||||
|
if (p.uuid in sensors or p.sensorname in sensors) \
|
||||||
|
and not p.sensorgroup_id:
|
||||||
|
values = {'sensorgroup_id': sensorgroup['id']}
|
||||||
|
# else if old sensor disassociated
|
||||||
|
elif ((p.uuid not in sensors and p.sensorname not in sensors) and
|
||||||
|
p.sensorgroup_id and
|
||||||
|
p.sensorgroup_id == this_sensorgroup_id):
|
||||||
|
values = {'sensorgroup_id': None}
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if p.datatype != this_sensorgroup_datatype:
|
||||||
|
msg = _("Invalid datatype: host {} sensor {}: Expected: {} "
|
||||||
|
"Received: {}.").format(
|
||||||
|
(ihost['hostname'], p.sensorname,
|
||||||
|
this_sensorgroup_datatype, p.datatype))
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
if p.sensortype != this_sensorgroup_sensortype:
|
||||||
|
msg = _("Invalid sensortype: host {} sensor {}: Expected: {} "
|
||||||
|
"Received: {}.").format(
|
||||||
|
ihost['hostname'], p.sensorname,
|
||||||
|
this_sensorgroup_sensortype, p.sensortype)
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pecan.request.dbapi.sensor_update(p.uuid, values)
|
||||||
|
except exception.HTTPNotFound:
|
||||||
|
msg = _("Sensor update of sensorgroup_uuid failed: host {} "
|
||||||
|
"sensor {}").format(ihost['hostname'], p.sensorname)
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
|
@ -0,0 +1,39 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2014 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
|
||||||
|
class State(base.APIBase):
|
||||||
|
|
||||||
|
current = wtypes.text
|
||||||
|
"The current state"
|
||||||
|
|
||||||
|
target = wtypes.text
|
||||||
|
"The user modified desired state"
|
||||||
|
|
||||||
|
available = [wtypes.text]
|
||||||
|
"A list of available states it is able to transition to"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"A list containing a self link and associated state links"
|
|
@ -0,0 +1,49 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from cgtsclient.v1 import client as cgts_client
|
||||||
|
from inventory.api import config
|
||||||
|
from keystoneauth1 import loading as ks_loading
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
_SESSION = None
|
||||||
|
|
||||||
|
|
||||||
|
def cgtsclient(context, version=1, endpoint=None):
|
||||||
|
"""Constructs a cgts client object for making API requests.
|
||||||
|
|
||||||
|
:param context: The FM request context for auth.
|
||||||
|
:param version: API endpoint version.
|
||||||
|
:param endpoint: Optional If the endpoint is not available, it will be
|
||||||
|
retrieved from session
|
||||||
|
"""
|
||||||
|
global _SESSION
|
||||||
|
|
||||||
|
if not _SESSION:
|
||||||
|
_SESSION = ks_loading.load_session_from_conf_options(
|
||||||
|
CONF, config.sysinv_group.name)
|
||||||
|
|
||||||
|
auth_token = context.auth_token
|
||||||
|
if endpoint is None:
|
||||||
|
auth = context.get_auth_plugin()
|
||||||
|
service_type, service_name, interface = \
|
||||||
|
CONF.sysinv.catalog_info.split(':')
|
||||||
|
service_parameters = {'service_type': service_type,
|
||||||
|
'service_name': service_name,
|
||||||
|
'interface': interface,
|
||||||
|
'region_name': CONF.sysinv.os_region_name}
|
||||||
|
endpoint = _SESSION.get_endpoint(auth, **service_parameters)
|
||||||
|
|
||||||
|
return cgts_client.Client(version=version,
|
||||||
|
endpoint=endpoint,
|
||||||
|
token=auth_token)
|
|
@ -0,0 +1,266 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
import six
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import base
|
||||||
|
from inventory.api.controllers.v1 import collection
|
||||||
|
from inventory.api.controllers.v1 import host
|
||||||
|
from inventory.api.controllers.v1 import link
|
||||||
|
from inventory.api.controllers.v1 import types
|
||||||
|
from inventory.api.controllers.v1 import utils as api_utils
|
||||||
|
from inventory.common import constants
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common import k_host
|
||||||
|
from inventory import objects
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
VALID_VSWITCH_TYPES = [constants.VSWITCH_TYPE_OVS_DPDK]
|
||||||
|
|
||||||
|
|
||||||
|
class System(base.APIBase):
|
||||||
|
"""API representation of a system.
|
||||||
|
|
||||||
|
This class enforces type checking and value constraints, and converts
|
||||||
|
between the internal object model and the API representation of
|
||||||
|
a system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid = types.uuid
|
||||||
|
"The UUID of the system"
|
||||||
|
|
||||||
|
name = wtypes.text
|
||||||
|
"The name of the system"
|
||||||
|
|
||||||
|
system_type = wtypes.text
|
||||||
|
"The type of the system"
|
||||||
|
|
||||||
|
system_mode = wtypes.text
|
||||||
|
"The mode of the system"
|
||||||
|
|
||||||
|
description = wtypes.text
|
||||||
|
"The name of the system"
|
||||||
|
|
||||||
|
contact = wtypes.text
|
||||||
|
"The contact of the system"
|
||||||
|
|
||||||
|
location = wtypes.text
|
||||||
|
"The location of the system"
|
||||||
|
|
||||||
|
services = int
|
||||||
|
"The services of the system"
|
||||||
|
|
||||||
|
software_version = wtypes.text
|
||||||
|
"A textual description of the entity"
|
||||||
|
|
||||||
|
timezone = wtypes.text
|
||||||
|
"The timezone of the system"
|
||||||
|
|
||||||
|
links = [link.Link]
|
||||||
|
"A list containing a self link and associated system links"
|
||||||
|
|
||||||
|
hosts = [link.Link]
|
||||||
|
"Links to the collection of hosts contained in this system"
|
||||||
|
|
||||||
|
capabilities = {wtypes.text: api_utils.ValidTypes(wtypes.text, bool,
|
||||||
|
six.integer_types)}
|
||||||
|
"System defined capabilities"
|
||||||
|
|
||||||
|
region_name = wtypes.text
|
||||||
|
"The region name of the system"
|
||||||
|
|
||||||
|
distributed_cloud_role = wtypes.text
|
||||||
|
"The distributed cloud role of the system"
|
||||||
|
|
||||||
|
service_project_name = wtypes.text
|
||||||
|
"The service project name of the system"
|
||||||
|
|
||||||
|
security_feature = wtypes.text
|
||||||
|
"Kernel arguments associated with enabled spectre/meltdown fix features"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.fields = objects.System.fields.keys()
|
||||||
|
|
||||||
|
for k in self.fields:
|
||||||
|
# Translate any special internal representation of data to its
|
||||||
|
# customer facing form
|
||||||
|
if k == 'security_feature':
|
||||||
|
# look up which customer-facing-security-feature-string goes
|
||||||
|
# with the kernel arguments tracked in sysinv
|
||||||
|
kernel_args = kwargs.get(k)
|
||||||
|
translated_string = kernel_args
|
||||||
|
|
||||||
|
for user_string, args_string in \
|
||||||
|
constants.SYSTEM_SECURITY_FEATURE_SPECTRE_MELTDOWN_OPTS.iteritems(): # noqa
|
||||||
|
if args_string == kernel_args:
|
||||||
|
translated_string = user_string
|
||||||
|
break
|
||||||
|
setattr(self, k, translated_string)
|
||||||
|
else:
|
||||||
|
# No translation required
|
||||||
|
setattr(self, k, kwargs.get(k))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, rpc_system, expand=True):
|
||||||
|
minimum_fields = ['id', 'uuid', 'name', 'system_type', 'system_mode',
|
||||||
|
'description', 'capabilities',
|
||||||
|
'contact', 'location', 'software_version',
|
||||||
|
'created_at', 'updated_at', 'timezone',
|
||||||
|
'region_name', 'service_project_name',
|
||||||
|
'distributed_cloud_role', 'security_feature']
|
||||||
|
|
||||||
|
fields = minimum_fields if not expand else None
|
||||||
|
|
||||||
|
iSystem = System.from_rpc_object(rpc_system, fields)
|
||||||
|
|
||||||
|
iSystem.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||||
|
'systems', iSystem.uuid),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'systems', iSystem.uuid,
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
if expand:
|
||||||
|
iSystem.hosts = [
|
||||||
|
link.Link.make_link('self',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'systems',
|
||||||
|
iSystem.uuid + "/hosts"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.host_url,
|
||||||
|
'systems',
|
||||||
|
iSystem.uuid + "/hosts",
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
|
return iSystem
|
||||||
|
|
||||||
|
|
||||||
|
class SystemCollection(collection.Collection):
|
||||||
|
"""API representation of a collection of systems."""
|
||||||
|
|
||||||
|
systems = [System]
|
||||||
|
"A list containing system objects"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'systems'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, systems, limit, url=None,
|
||||||
|
expand=False, **kwargs):
|
||||||
|
collection = SystemCollection()
|
||||||
|
collection.systems = [System.convert_with_links(ch, expand)
|
||||||
|
for ch in systems]
|
||||||
|
|
||||||
|
collection.next = collection.get_next(limit, url=url, **kwargs)
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
|
LOCK_NAME = 'SystemController'
|
||||||
|
|
||||||
|
|
||||||
|
class SystemController(rest.RestController):
|
||||||
|
"""REST controller for system."""
|
||||||
|
|
||||||
|
hosts = host.HostController(from_system=True)
|
||||||
|
"Expose hosts as a sub-element of system"
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'detail': ['GET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._bm_region = None
|
||||||
|
|
||||||
|
def _bm_region_get(self):
|
||||||
|
# only supported region type is BM_EXTERNAL
|
||||||
|
if not self._bm_region:
|
||||||
|
self._bm_region = k_host.BM_EXTERNAL
|
||||||
|
return self._bm_region
|
||||||
|
|
||||||
|
def _get_system_collection(self, marker, limit, sort_key, sort_dir,
|
||||||
|
expand=False, resource_url=None):
|
||||||
|
limit = api_utils.validate_limit(limit)
|
||||||
|
sort_dir = api_utils.validate_sort_dir(sort_dir)
|
||||||
|
marker_obj = None
|
||||||
|
if marker:
|
||||||
|
marker_obj = objects.System.get_by_uuid(pecan.request.context,
|
||||||
|
marker)
|
||||||
|
system = pecan.request.dbapi.system_get_list(limit, marker_obj,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
for i in system:
|
||||||
|
i.capabilities['bm_region'] = self._bm_region_get()
|
||||||
|
|
||||||
|
return SystemCollection.convert_with_links(system, limit,
|
||||||
|
url=resource_url,
|
||||||
|
expand=expand,
|
||||||
|
sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(SystemCollection, types.uuid,
|
||||||
|
int, wtypes.text, wtypes.text)
|
||||||
|
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of systems.
|
||||||
|
|
||||||
|
:param marker: pagination marker for large data sets.
|
||||||
|
:param limit: maximum number of resources to return in a single result.
|
||||||
|
:param sort_key: column to sort results by. Default: id.
|
||||||
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
|
"""
|
||||||
|
return self._get_system_collection(marker, limit, sort_key, sort_dir)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(SystemCollection, types.uuid, int,
|
||||||
|
wtypes.text, wtypes.text)
|
||||||
|
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
|
||||||
|
"""Retrieve a list of system with detail.
|
||||||
|
|
||||||
|
:param marker: pagination marker for large data sets.
|
||||||
|
:param limit: maximum number of resources to return in a single result.
|
||||||
|
:param sort_key: column to sort results by. Default: id.
|
||||||
|
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
|
||||||
|
"""
|
||||||
|
# /detail should only work agaist collections
|
||||||
|
parent = pecan.request.path.split('/')[:-1][-1]
|
||||||
|
if parent != "system":
|
||||||
|
raise exception.HTTPNotFound
|
||||||
|
|
||||||
|
expand = True
|
||||||
|
resource_url = '/'.join(['system', 'detail'])
|
||||||
|
return self._get_system_collection(marker, limit, sort_key, sort_dir,
|
||||||
|
expand, resource_url)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(System, types.uuid)
|
||||||
|
def get_one(self, system_uuid):
|
||||||
|
"""Retrieve information about the given system.
|
||||||
|
|
||||||
|
:param system_uuid: UUID of a system.
|
||||||
|
"""
|
||||||
|
rpc_system = objects.System.get_by_uuid(pecan.request.context,
|
||||||
|
system_uuid)
|
||||||
|
|
||||||
|
rpc_system.capabilities['bm_region'] = self._bm_region_get()
|
||||||
|
return System.convert_with_links(rpc_system)
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(System, body=System)
|
||||||
|
def post(self, system):
|
||||||
|
"""Create a new system."""
|
||||||
|
raise exception.OperationNotPermitted
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
|
def delete(self, system_uuid):
|
||||||
|
"""Delete a system.
|
||||||
|
|
||||||
|
:param system_uuid: UUID of a system.
|
||||||
|
"""
|
||||||
|
raise exception.OperationNotPermitted
|
|
@ -0,0 +1,215 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
# coding: utf-8
|
||||||
|
#
|
||||||
|
# Copyright 2013 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
from oslo_utils import strutils
|
||||||
|
import six
|
||||||
|
|
||||||
|
import wsme
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1 import utils as apiutils
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
class MACAddressType(wtypes.UserType):
|
||||||
|
"""A simple MAC address type."""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'macaddress'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
return utils.validate_and_normalize_mac(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
return MACAddressType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
|
class UUIDType(wtypes.UserType):
|
||||||
|
"""A simple UUID type."""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'uuid'
|
||||||
|
# FIXME(lucasagomes): When used with wsexpose decorator WSME will try
|
||||||
|
# to get the name of the type by accessing it's __name__ attribute.
|
||||||
|
# Remove this __name__ attribute once it's fixed in WSME.
|
||||||
|
# https://bugs.launchpad.net/wsme/+bug/1265590
|
||||||
|
__name__ = name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
if not utils.is_uuid_like(value):
|
||||||
|
raise exception.InvalidUUID(uuid=value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return UUIDType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanType(wtypes.UserType):
|
||||||
|
"""A simple boolean type."""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'boolean'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
try:
|
||||||
|
return strutils.bool_from_string(value, strict=True)
|
||||||
|
except ValueError as e:
|
||||||
|
# raise Invalid to return 400 (BadRequest) in the API
|
||||||
|
raise exception.Invalid(six.text_type(e))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return BooleanType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressType(wtypes.UserType):
|
||||||
|
"""A generic IP address type that supports both IPv4 and IPv6."""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'ipaddress'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
if not utils.is_valid_ip(value):
|
||||||
|
raise exception.InvalidIPAddress(address=value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return IPAddressType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
|
macaddress = MACAddressType()
|
||||||
|
uuid = UUIDType()
|
||||||
|
boolean = BooleanType()
|
||||||
|
ipaddress = IPAddressType()
|
||||||
|
|
||||||
|
|
||||||
|
class ApiDictType(wtypes.UserType):
|
||||||
|
name = 'apidict'
|
||||||
|
__name__ = name
|
||||||
|
|
||||||
|
basetype = {wtypes.text:
|
||||||
|
apiutils.ValidTypes(wtypes.text, six.integer_types)}
|
||||||
|
|
||||||
|
|
||||||
|
apidict = ApiDictType()
|
||||||
|
|
||||||
|
|
||||||
|
class JsonPatchType(wtypes.Base):
|
||||||
|
"""A complex type that represents a single json-patch operation."""
|
||||||
|
|
||||||
|
path = wtypes.wsattr(wtypes.StringType(pattern='^(/[\w-]+)+$'),
|
||||||
|
mandatory=True)
|
||||||
|
op = wtypes.wsattr(wtypes.Enum(str, 'add', 'replace', 'remove'),
|
||||||
|
mandatory=True)
|
||||||
|
value = apiutils.ValidTypes(wtypes.text, six.integer_types, float)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def internal_attrs():
|
||||||
|
"""Returns a list of internal attributes.
|
||||||
|
|
||||||
|
Internal attributes can't be added, replaced or removed. This
|
||||||
|
method may be overwritten by derived class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return ['/created_at', '/id', '/links', '/updated_at', '/uuid']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mandatory_attrs():
|
||||||
|
"""Retruns a list of mandatory attributes.
|
||||||
|
|
||||||
|
Mandatory attributes can't be removed from the document. This
|
||||||
|
method should be overwritten by derived class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(patch):
|
||||||
|
if patch.path in patch.internal_attrs():
|
||||||
|
msg = _("'%s' is an internal attribute and can not be updated")
|
||||||
|
raise wsme.exc.ClientSideError(msg % patch.path)
|
||||||
|
|
||||||
|
if patch.path in patch.mandatory_attrs() and patch.op == 'remove':
|
||||||
|
msg = _("'%s' is a mandatory attribute and can not be removed")
|
||||||
|
raise wsme.exc.ClientSideError(msg % patch.path)
|
||||||
|
|
||||||
|
if patch.op == 'add':
|
||||||
|
if patch.path.count('/') == 1:
|
||||||
|
msg = _('Adding a new attribute (%s) to the root of '
|
||||||
|
' the resource is not allowed')
|
||||||
|
raise wsme.exc.ClientSideError(msg % patch.path)
|
||||||
|
|
||||||
|
if patch.op != 'remove':
|
||||||
|
if not patch.value:
|
||||||
|
msg = _("Edit and Add operation of the field requires "
|
||||||
|
"non-empty value.")
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
ret = {'path': patch.path, 'op': patch.op}
|
||||||
|
if patch.value:
|
||||||
|
ret['value'] = patch.value
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class MultiType(wtypes.UserType):
|
||||||
|
"""A complex type that represents one or more types.
|
||||||
|
|
||||||
|
Used for validating that a value is an instance of one of the types.
|
||||||
|
|
||||||
|
:param *types: Variable-length list of types.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, types):
|
||||||
|
self.types = types
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
for t in self.types:
|
||||||
|
if t is wsme.types.text and isinstance(value, wsme.types.bytes):
|
||||||
|
value = value.decode()
|
||||||
|
if isinstance(t, list):
|
||||||
|
if isinstance(value, list):
|
||||||
|
for v in value:
|
||||||
|
if not isinstance(v, t[0]):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
elif isinstance(value, t):
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
_("Wrong type. Expected '%(type)s', got '%(value)s'")
|
||||||
|
% {'type': self.types, 'value': type(value)})
|
|
@ -0,0 +1,567 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import jsonpatch
|
||||||
|
import netaddr
|
||||||
|
import os
|
||||||
|
import pecan
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import tsconfig.tsconfig as tsc
|
||||||
|
import wsme
|
||||||
|
|
||||||
|
from inventory.api.controllers.v1.sysinv import cgtsclient
|
||||||
|
from inventory.common import constants
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import k_host
|
||||||
|
from inventory.common.utils import memoized
|
||||||
|
from inventory import objects
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
KEY_VALUE_SEP = '='
|
||||||
|
JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException,
|
||||||
|
jsonpatch.JsonPointerException,
|
||||||
|
KeyError)
|
||||||
|
|
||||||
|
|
||||||
|
def ip_version_to_string(ip_version):
|
||||||
|
return str(constants.IP_FAMILIES[ip_version])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_limit(limit):
|
||||||
|
if limit and limit < 0:
|
||||||
|
raise wsme.exc.ClientSideError(_("Limit must be positive"))
|
||||||
|
|
||||||
|
return min(CONF.api.limit_max, limit) or CONF.api.limit_max
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sort_dir(sort_dir):
|
||||||
|
if sort_dir not in ['asc', 'desc']:
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. "
|
||||||
|
"Acceptable values are "
|
||||||
|
"'asc' or 'desc'") % sort_dir)
|
||||||
|
return sort_dir
|
||||||
|
|
||||||
|
|
||||||
|
def validate_patch(patch):
|
||||||
|
"""Performs a basic validation on patch."""
|
||||||
|
|
||||||
|
if not isinstance(patch, list):
|
||||||
|
patch = [patch]
|
||||||
|
|
||||||
|
for p in patch:
|
||||||
|
path_pattern = re.compile("^/[a-zA-Z0-9-_]+(/[a-zA-Z0-9-_]+)*$")
|
||||||
|
|
||||||
|
if not isinstance(p, dict) or \
|
||||||
|
any(key for key in ["path", "op"] if key not in p):
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("Invalid patch format: %s") % str(p))
|
||||||
|
|
||||||
|
path = p["path"]
|
||||||
|
op = p["op"]
|
||||||
|
|
||||||
|
if op not in ["add", "replace", "remove"]:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("Operation not supported: %s") % op)
|
||||||
|
|
||||||
|
if not path_pattern.match(path):
|
||||||
|
raise wsme.exc.ClientSideError(_("Invalid path: %s") % path)
|
||||||
|
|
||||||
|
if op == "add":
|
||||||
|
if path.count('/') == 1:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
_("Adding an additional attribute (%s) to the "
|
||||||
|
"resource is not allowed") % path)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_mtu(mtu):
|
||||||
|
"""Check if MTU is valid"""
|
||||||
|
if mtu < 576 or mtu > 9216:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"MTU must be between 576 and 9216 bytes."))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_address_within_address_pool(ip, pool):
|
||||||
|
"""Determine whether an IP address is within the specified IP address pool.
|
||||||
|
:param ip netaddr.IPAddress object
|
||||||
|
:param pool objects.AddressPool object
|
||||||
|
"""
|
||||||
|
ipset = netaddr.IPSet()
|
||||||
|
for start, end in pool.ranges:
|
||||||
|
ipset.update(netaddr.IPRange(start, end))
|
||||||
|
|
||||||
|
if netaddr.IPAddress(ip) not in ipset:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"IP address %s is not within address pool ranges") % str(ip))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_address_within_nework(ip, network):
|
||||||
|
"""Determine whether an IP address is within the specified IP network.
|
||||||
|
:param ip netaddr.IPAddress object
|
||||||
|
:param network objects.Network object
|
||||||
|
"""
|
||||||
|
LOG.info("TODO(sc) validate_address_within_address_pool "
|
||||||
|
"ip=%s, network=%s" % (ip, network))
|
||||||
|
|
||||||
|
|
||||||
|
class ValidTypes(wsme.types.UserType):
|
||||||
|
"""User type for validate that value has one of a few types."""
|
||||||
|
|
||||||
|
def __init__(self, *types):
|
||||||
|
self.types = types
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
for t in self.types:
|
||||||
|
if t is wsme.types.text and isinstance(value, wsme.types.bytes):
|
||||||
|
value = value.decode()
|
||||||
|
if isinstance(value, t):
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise ValueError("Wrong type. Expected '%s', got '%s'" % (
|
||||||
|
self.types, type(value)))
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_hostname(hostname):
|
||||||
|
"""Determine whether an address is valid as per RFC 1123.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Maximum length of 255
|
||||||
|
rc = True
|
||||||
|
length = len(hostname)
|
||||||
|
if length > 255:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"Hostname {} is too long. Length {} is greater than 255."
|
||||||
|
"Please configure valid hostname.").format(hostname, length))
|
||||||
|
|
||||||
|
# Allow a single dot on the right hand side
|
||||||
|
if hostname[-1] == ".":
|
||||||
|
hostname = hostname[:-1]
|
||||||
|
# Create a regex to ensure:
|
||||||
|
# - hostname does not begin or end with a dash
|
||||||
|
# - each segment is 1 to 63 characters long
|
||||||
|
# - valid characters are A-Z (any case) and 0-9
|
||||||
|
valid_re = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
|
||||||
|
rc = all(valid_re.match(x) for x in hostname.split("."))
|
||||||
|
if not rc:
|
||||||
|
raise wsme.exc.ClientSideError(_(
|
||||||
|
"Hostname %s is invalid. Hostname may not begin or end with"
|
||||||
|
" a dash. Each segment is 1 to 63 chars long and valid"
|
||||||
|
" characters are A-Z, a-z, and 0-9."
|
||||||
|
" Please configure valid hostname.") % (hostname))
|
||||||
|
|
||||||
|
return rc
|
||||||
|
|
||||||
|
|
||||||
|
def is_host_active_controller(host):
|
||||||
|
"""Returns True if the supplied host is the active controller."""
|
||||||
|
if host['personality'] == k_host.CONTROLLER:
|
||||||
|
return host['hostname'] == socket.gethostname()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_host_simplex_controller(host):
|
||||||
|
return host['personality'] == k_host.CONTROLLER and \
|
||||||
|
os.path.isfile(tsc.PLATFORM_SIMPLEX_FLAG)
|
||||||
|
|
||||||
|
|
||||||
|
def is_aio_simplex_host_unlocked(host):
|
||||||
|
return (get_system_mode() == constants.SYSTEM_MODE_SIMPLEX and
|
||||||
|
host['administrative'] != k_host.ADMIN_LOCKED and
|
||||||
|
host['invprovision'] != k_host.PROVISIONING)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vswitch_type():
|
||||||
|
system = pecan.request.dbapi.system_get_one()
|
||||||
|
return system.capabilities.get('vswitch_type')
|
||||||
|
|
||||||
|
|
||||||
|
def get_https_enabled():
|
||||||
|
system = pecan.request.dbapi.system_get_one()
|
||||||
|
return system.capabilities.get('https_enabled', False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tpm_config():
|
||||||
|
tpmconfig = None
|
||||||
|
try:
|
||||||
|
tpmconfig = pecan.request.dbapi.tpmconfig_get_one()
|
||||||
|
except exception.InventoryException:
|
||||||
|
pass
|
||||||
|
return tpmconfig
|
||||||
|
|
||||||
|
|
||||||
|
def get_sdn_enabled():
|
||||||
|
system = pecan.request.dbapi.system_get_one()
|
||||||
|
return system.capabilities.get('sdn_enabled', False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_region_config():
|
||||||
|
system = pecan.request.dbapi.system_get_one()
|
||||||
|
# TODO(mpeters): this should to be updated to return a boolean value
|
||||||
|
# requires integration changes between horizon, cgts-client and users to
|
||||||
|
# transition to a proper boolean value
|
||||||
|
return system.capabilities.get('region_config', False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_shared_services():
|
||||||
|
system = pecan.request.dbapi.system_get_one()
|
||||||
|
return system.capabilities.get('shared_services', None)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemHelper(object):
|
||||||
|
@staticmethod
|
||||||
|
def get_product_build():
|
||||||
|
active_controller = HostHelper.get_active_controller()
|
||||||
|
if k_host.COMPUTE in active_controller.subfunctions:
|
||||||
|
return constants.TIS_AIO_BUILD
|
||||||
|
return constants.TIS_STD_BUILD
|
||||||
|
|
||||||
|
|
||||||
|
class HostHelper(object):
|
||||||
|
@staticmethod
|
||||||
|
@memoized
|
||||||
|
def get_active_controller(dbapi=None):
|
||||||
|
"""Returns host object for active controller."""
|
||||||
|
if not dbapi:
|
||||||
|
dbapi = pecan.request.dbapi
|
||||||
|
hosts = objects.Host.list(pecan.request.context,
|
||||||
|
filters={'personality': k_host.CONTROLLER})
|
||||||
|
active_controller = None
|
||||||
|
for host in hosts:
|
||||||
|
if is_host_active_controller(host):
|
||||||
|
active_controller = host
|
||||||
|
break
|
||||||
|
|
||||||
|
return active_controller
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_mode(dbapi=None):
|
||||||
|
if not dbapi:
|
||||||
|
dbapi = pecan.request.dbapi
|
||||||
|
system = dbapi.system_get_one()
|
||||||
|
return system.system_mode
|
||||||
|
|
||||||
|
|
||||||
|
def get_distributed_cloud_role(dbapi=None):
|
||||||
|
if not dbapi:
|
||||||
|
dbapi = pecan.request.dbapi
|
||||||
|
system = dbapi.system_get_one()
|
||||||
|
return system.distributed_cloud_role
|
||||||
|
|
||||||
|
|
||||||
|
def is_kubernetes_config(dbapi=None):
|
||||||
|
if not dbapi:
|
||||||
|
dbapi = pecan.request.dbapi
|
||||||
|
system = dbapi.system_get_one()
|
||||||
|
return system.capabilities.get('kubernetes_enabled', False)
|
||||||
|
|
||||||
|
|
||||||
|
def is_aio_duplex_system():
|
||||||
|
return get_system_mode() == constants.SYSTEM_MODE_DUPLEX and \
|
||||||
|
SystemHelper.get_product_build() == constants.TIS_AIO_BUILD
|
||||||
|
|
||||||
|
|
||||||
|
def get_compute_count(dbapi=None):
|
||||||
|
if not dbapi:
|
||||||
|
dbapi = pecan.request.dbapi
|
||||||
|
return len(dbapi.host_get_by_personality(k_host.COMPUTE))
|
||||||
|
|
||||||
|
|
||||||
|
class SBApiHelper(object):
|
||||||
|
"""API Helper Class for manipulating Storage Backends.
|
||||||
|
|
||||||
|
Common functionality needed by the storage_backend API and it's derived
|
||||||
|
APIs: storage_ceph, storage_lvm, storage_file.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def validate_backend(storage_backend_dict):
|
||||||
|
|
||||||
|
backend = storage_backend_dict.get('backend')
|
||||||
|
if not backend:
|
||||||
|
raise wsme.exc.ClientSideError("This operation requires a "
|
||||||
|
"storage backend to be specified.")
|
||||||
|
|
||||||
|
if backend not in constants.SB_SUPPORTED:
|
||||||
|
raise wsme.exc.ClientSideError("Supplied storage backend (%s) is "
|
||||||
|
"not supported." % backend)
|
||||||
|
|
||||||
|
name = storage_backend_dict.get('name')
|
||||||
|
if not name:
|
||||||
|
# Get the list of backends of this type. If none are present, then
|
||||||
|
# this is the system default backend for this type. Therefore use
|
||||||
|
# the default name.
|
||||||
|
backend_list = \
|
||||||
|
pecan.request.dbapi.storage_backend_get_list_by_type(
|
||||||
|
backend_type=backend)
|
||||||
|
if not backend_list:
|
||||||
|
storage_backend_dict['name'] = constants.SB_DEFAULT_NAMES[
|
||||||
|
backend]
|
||||||
|
else:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
"This operation requires storage "
|
||||||
|
"backend name to be specified.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def common_checks(operation, storage_backend_dict):
|
||||||
|
backend = SBApiHelper.validate_backend(storage_backend_dict)
|
||||||
|
|
||||||
|
backend_type = storage_backend_dict['backend']
|
||||||
|
backend_name = storage_backend_dict['name']
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_backend = pecan.request.dbapi.storage_backend_get_by_name(
|
||||||
|
backend_name)
|
||||||
|
except exception.StorageBackendNotFoundByName:
|
||||||
|
existing_backend = None
|
||||||
|
|
||||||
|
# The "shared_services" of an external backend can't have any internal
|
||||||
|
# backend, vice versa. Note: This code needs to be revisited when
|
||||||
|
# "non_shared_services" external backend (e.g. emc) is added into
|
||||||
|
# storage-backend.
|
||||||
|
if operation in [
|
||||||
|
constants.SB_API_OP_CREATE, constants.SB_API_OP_MODIFY]:
|
||||||
|
current_bk_svcs = []
|
||||||
|
backends = pecan.request.dbapi.storage_backend_get_list()
|
||||||
|
for bk in backends:
|
||||||
|
if backend_type == constants.SB_TYPE_EXTERNAL:
|
||||||
|
if bk.as_dict()['backend'] != backend_type:
|
||||||
|
current_bk_svcs += \
|
||||||
|
SBApiHelper.getListFromServices(bk.as_dict())
|
||||||
|
else:
|
||||||
|
if bk.as_dict()['backend'] == constants.SB_TYPE_EXTERNAL:
|
||||||
|
current_bk_svcs += \
|
||||||
|
SBApiHelper.getListFromServices(bk.as_dict())
|
||||||
|
|
||||||
|
new_bk_svcs = SBApiHelper.getListFromServices(storage_backend_dict)
|
||||||
|
for svc in new_bk_svcs:
|
||||||
|
if svc in current_bk_svcs:
|
||||||
|
raise wsme.exc.ClientSideError("Service (%s) already has "
|
||||||
|
"a backend." % svc)
|
||||||
|
|
||||||
|
# Deny any change while a backend is configuring
|
||||||
|
backends = pecan.request.dbapi.storage_backend_get_list()
|
||||||
|
for bk in backends:
|
||||||
|
if bk['state'] == constants.SB_STATE_CONFIGURING:
|
||||||
|
msg = _("%s backend is configuring, please wait for "
|
||||||
|
"current operation to complete before making "
|
||||||
|
"changes.") % bk['backend'].title()
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
if not existing_backend:
|
||||||
|
existing_backends_by_type = set(bk['backend'] for bk in backends)
|
||||||
|
|
||||||
|
if (backend_type in existing_backends_by_type and
|
||||||
|
backend_type not in [
|
||||||
|
constants.SB_TYPE_CEPH,
|
||||||
|
constants.SB_TYPE_CEPH_EXTERNAL]):
|
||||||
|
msg = _("Only one %s backend is supported.") % backend_type
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
elif (backend_type != constants.SB_TYPE_CEPH_EXTERNAL and
|
||||||
|
backend_type not in existing_backends_by_type and
|
||||||
|
backend_name != constants.SB_DEFAULT_NAMES[backend_type]):
|
||||||
|
msg = _("The primary {} backend must use the "
|
||||||
|
"default name: {}.").format(
|
||||||
|
backend_type,
|
||||||
|
constants.SB_DEFAULT_NAMES[backend_type])
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
# Deny operations with a single, unlocked, controller.
|
||||||
|
# TODO(oponcea): Remove this once sm supports in-service config reload
|
||||||
|
ctrls = objects.Host.list(pecan.request.context,
|
||||||
|
filters={'personality': k_host.CONTROLLER})
|
||||||
|
if len(ctrls) == 1:
|
||||||
|
if ctrls[0].administrative == k_host.ADMIN_UNLOCKED:
|
||||||
|
if get_system_mode() == constants.SYSTEM_MODE_SIMPLEX:
|
||||||
|
msg = _("Storage backend operations require controller "
|
||||||
|
"host to be locked.")
|
||||||
|
else:
|
||||||
|
msg = _("Storage backend operations require "
|
||||||
|
"both controllers to be enabled and available.")
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
else:
|
||||||
|
for ctrl in ctrls:
|
||||||
|
if ctrl.availability not in [k_host.AVAILABILITY_AVAILABLE,
|
||||||
|
k_host.AVAILABILITY_DEGRADED]:
|
||||||
|
msg = _("Storage backend operations require "
|
||||||
|
"both controllers "
|
||||||
|
"to be enabled and available/degraded.")
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
if existing_backend and operation == constants.SB_API_OP_CREATE:
|
||||||
|
if (existing_backend.state == constants.SB_STATE_CONFIGURED or
|
||||||
|
existing_backend.state == constants.SB_STATE_CONFIG_ERR):
|
||||||
|
msg = (
|
||||||
|
_("Initial (%s) backend was previously created. Use the "
|
||||||
|
"modify API for further provisioning or supply a unique "
|
||||||
|
"name to add an additional backend.") %
|
||||||
|
existing_backend.name)
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
elif not existing_backend and operation == constants.SB_API_OP_MODIFY:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
"Attempting to modify non-existant (%s) backend." % backend)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_backend_data(requested, defaults, checks, supported_svcs,
|
||||||
|
current=None):
|
||||||
|
"""Returns a valid backend dictionary based on current inputs
|
||||||
|
|
||||||
|
:param requested: data from the API
|
||||||
|
:param defaults: values that should be set if missing or
|
||||||
|
not currently set
|
||||||
|
:param checks: a set of valid data to be mapped into the
|
||||||
|
backend capabilities
|
||||||
|
:param supported_svcs: services that are allowed to be used
|
||||||
|
with this backend
|
||||||
|
:param current: the existing view of this data (typically from the DB)
|
||||||
|
"""
|
||||||
|
if current:
|
||||||
|
merged = current.copy()
|
||||||
|
else:
|
||||||
|
merged = requested.copy()
|
||||||
|
|
||||||
|
# go through the requested values
|
||||||
|
for key in requested:
|
||||||
|
if key in merged and merged[key] != requested[key]:
|
||||||
|
merged[key] = requested[key]
|
||||||
|
|
||||||
|
# Set existing defaults
|
||||||
|
for key in merged:
|
||||||
|
if merged[key] is None and key in defaults:
|
||||||
|
merged[key] = defaults[key]
|
||||||
|
|
||||||
|
# Add the missing defaults
|
||||||
|
for key in defaults:
|
||||||
|
if key not in merged:
|
||||||
|
merged[key] = defaults[key]
|
||||||
|
|
||||||
|
# Pop the current set of data and make sure only supported parameters
|
||||||
|
# are populated
|
||||||
|
hiera_data = merged.pop('capabilities', {})
|
||||||
|
merged['capabilities'] = {}
|
||||||
|
|
||||||
|
merged_hiera_data = defaults.pop('capabilities', {})
|
||||||
|
merged_hiera_data.update(hiera_data)
|
||||||
|
|
||||||
|
for key in merged_hiera_data:
|
||||||
|
if key in checks['backend']:
|
||||||
|
merged['capabilities'][key] = merged_hiera_data[key]
|
||||||
|
continue
|
||||||
|
for svc in supported_svcs:
|
||||||
|
if key in checks[svc]:
|
||||||
|
merged['capabilities'][key] = merged_hiera_data[key]
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_minimal_number_of_controllers(min_number):
|
||||||
|
chosts = pecan.request.dbapi.host_get_by_personality(
|
||||||
|
k_host.CONTROLLER)
|
||||||
|
|
||||||
|
if len(chosts) < min_number:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
"This operation requires %s controllers provisioned." %
|
||||||
|
min_number)
|
||||||
|
|
||||||
|
for chost in chosts:
|
||||||
|
if chost.invprovision != k_host.PROVISIONED:
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
"This operation requires %s controllers provisioned." %
|
||||||
|
min_number)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getListFromServices(be_dict):
|
||||||
|
return [] if be_dict['services'] is None \
|
||||||
|
else be_dict['services'].split(',')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def setServicesFromList(be_dict, svc_list):
|
||||||
|
be_dict['services'] = ','.join(svc_list)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_svc_enabled(sb_list, svc):
|
||||||
|
for b in sb_list:
|
||||||
|
if b.services:
|
||||||
|
if svc in b.services:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def enable_backend(sb, backend_enable_function):
|
||||||
|
"""In-service enable storage backend """
|
||||||
|
try:
|
||||||
|
# Initiate manifest application
|
||||||
|
LOG.info(_("Initializing configuration of storage %s backend.") %
|
||||||
|
sb.backend.title())
|
||||||
|
backend_enable_function(pecan.request.context)
|
||||||
|
LOG.info("Configuration of storage %s backend initialized, "
|
||||||
|
"continuing in background." % sb.backend.title())
|
||||||
|
except exception.InventoryException:
|
||||||
|
LOG.exception("Manifests failed!")
|
||||||
|
# Set lvm backend to error so that it can be recreated
|
||||||
|
values = {'state': constants.SB_STATE_CONFIG_ERR, 'task': None}
|
||||||
|
pecan.request.dbapi.storage_backend_update(sb.uuid, values)
|
||||||
|
msg = (_("%s configuration failed, check node status and retry. "
|
||||||
|
"If problem persists contact next level of support.") %
|
||||||
|
sb.backend.title())
|
||||||
|
raise wsme.exc.ClientSideError(msg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_primary_ceph_tier(name_string):
|
||||||
|
"""Check if a tier name string is for the primary ceph tier. """
|
||||||
|
if name_string == constants.SB_TIER_DEFAULT_NAMES[
|
||||||
|
constants.SB_TYPE_CEPH]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_primary_ceph_backend(name_string):
|
||||||
|
"""Check if a backend name string is for the primary ceph backend. """
|
||||||
|
if name_string == constants.SB_DEFAULT_NAMES[constants.SB_TYPE_CEPH]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def save_and_reraise_exception():
|
||||||
|
"""Save current exception, run some code and then re-raise.
|
||||||
|
|
||||||
|
In some cases the exception context can be cleared, resulting in None
|
||||||
|
being attempted to be re-raised after an exception handler is run. This
|
||||||
|
can happen when eventlet switches greenthreads or when running an
|
||||||
|
exception handler, code raises and catches an exception. In both
|
||||||
|
cases the exception context will be cleared.
|
||||||
|
|
||||||
|
To work around this, we save the exception state, run handler code, and
|
||||||
|
then re-raise the original exception. If another exception occurs, the
|
||||||
|
saved exception is logged and the new exception is re-raised.
|
||||||
|
"""
|
||||||
|
type_, value, tb = sys.exc_info()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception:
|
||||||
|
LOG.error(_('Original exception being dropped: %s'),
|
||||||
|
traceback.format_exception(type_, value, tb))
|
||||||
|
raise
|
||||||
|
raise (type_, value, tb)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_port(host_name, port_name):
|
||||||
|
hosts = cgtsclient(pecan.request.context).ihost.list()
|
||||||
|
for h in hosts:
|
||||||
|
if h.hostname == host_name:
|
||||||
|
ports = cgtsclient(pecan.request.context).port.list(h.uuid)
|
||||||
|
for p in ports:
|
||||||
|
if p.name == port_name:
|
||||||
|
return p
|
||||||
|
return None
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Copyright (c) 2015 Intel Corporation
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
# This is the version 1 API
|
||||||
|
BASE_VERSION = 1
|
||||||
|
|
||||||
|
# Here goes a short log of changes in every version.
|
||||||
|
# Refer to doc/source/dev/webapi-version-history.rst for a detailed explanation
|
||||||
|
# of what each version contains.
|
||||||
|
#
|
||||||
|
# v1.0: corresponds to Initial API
|
||||||
|
|
||||||
|
MINOR_0_INITIAL_VERSION = 0
|
||||||
|
|
||||||
|
# When adding another version, update:
|
||||||
|
# - MINOR_MAX_VERSION
|
||||||
|
# - doc/source/contributor/webapi-version-history.rst with a detailed
|
||||||
|
# explanation of what changed in the new version
|
||||||
|
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
||||||
|
|
||||||
|
MINOR_MAX_VERSION = MINOR_0_INITIAL_VERSION
|
||||||
|
|
||||||
|
# String representations of the minor and maximum versions
|
||||||
|
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_0_INITIAL_VERSION)
|
||||||
|
_MAX_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_MAX_VERSION)
|
||||||
|
|
||||||
|
|
||||||
|
def min_version_string():
|
||||||
|
"""Returns the minimum supported API version (as a string)"""
|
||||||
|
return _MIN_VERSION_STRING
|
||||||
|
|
||||||
|
|
||||||
|
def max_version_string():
|
||||||
|
"""Returns the maximum supported API version (as a string).
|
||||||
|
|
||||||
|
If the service is pinned, the maximum API version is the pinned
|
||||||
|
version. Otherwise, it is the maximum supported API version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO(jkung): enable when release versions supported
|
||||||
|
# release_ver = release_mappings.RELEASE_MAPPING.get(
|
||||||
|
# CONF.pin_release_version)
|
||||||
|
# if release_ver:
|
||||||
|
# return release_ver['api']
|
||||||
|
# else:
|
||||||
|
return _MAX_VERSION_STRING
|
|
@ -0,0 +1,110 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
from inventory.common import context
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.conductor import rpcapi
|
||||||
|
from inventory.db import api as dbapi
|
||||||
|
from inventory.systemconfig import plugin as systemconfig_plugin
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
from pecan import hooks
|
||||||
|
import webob
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ContextHook(hooks.PecanHook):
|
||||||
|
"""Configures a request context and attaches it to the request.
|
||||||
|
|
||||||
|
The following HTTP request headers are used:
|
||||||
|
|
||||||
|
X-User-Name:
|
||||||
|
Used for context.user_name.
|
||||||
|
|
||||||
|
X-User-Id:
|
||||||
|
Used for context.user_id.
|
||||||
|
|
||||||
|
X-Project-Name:
|
||||||
|
Used for context.project.
|
||||||
|
|
||||||
|
X-Project-Id:
|
||||||
|
Used for context.project_id.
|
||||||
|
|
||||||
|
X-Auth-Token:
|
||||||
|
Used for context.auth_token.
|
||||||
|
|
||||||
|
X-Roles:
|
||||||
|
Used for context.roles.
|
||||||
|
|
||||||
|
X-Service_Catalog:
|
||||||
|
Used for context.service_catalog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def before(self, state):
|
||||||
|
headers = state.request.headers
|
||||||
|
environ = state.request.environ
|
||||||
|
user_name = headers.get('X-User-Name')
|
||||||
|
user_id = headers.get('X-User-Id')
|
||||||
|
project = headers.get('X-Project-Name')
|
||||||
|
project_id = headers.get('X-Project-Id')
|
||||||
|
domain_id = headers.get('X-User-Domain-Id')
|
||||||
|
domain_name = headers.get('X-User-Domain-Name')
|
||||||
|
auth_token = headers.get('X-Auth-Token')
|
||||||
|
roles = headers.get('X-Roles', '').split(',')
|
||||||
|
catalog_header = headers.get('X-Service-Catalog')
|
||||||
|
service_catalog = None
|
||||||
|
if catalog_header:
|
||||||
|
try:
|
||||||
|
service_catalog = jsonutils.loads(catalog_header)
|
||||||
|
except ValueError:
|
||||||
|
raise webob.exc.HTTPInternalServerError(
|
||||||
|
_('Invalid service catalog json.'))
|
||||||
|
|
||||||
|
auth_token_info = environ.get('keystone.token_info')
|
||||||
|
auth_url = CONF.keystone_authtoken.auth_uri
|
||||||
|
|
||||||
|
state.request.context = context.make_context(
|
||||||
|
auth_token=auth_token,
|
||||||
|
auth_url=auth_url,
|
||||||
|
auth_token_info=auth_token_info,
|
||||||
|
user_name=user_name,
|
||||||
|
user_id=user_id,
|
||||||
|
project_name=project,
|
||||||
|
project_id=project_id,
|
||||||
|
domain_id=domain_id,
|
||||||
|
domain_name=domain_name,
|
||||||
|
roles=roles,
|
||||||
|
service_catalog=service_catalog
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DBHook(hooks.PecanHook):
|
||||||
|
"""Attach the dbapi object to the request so controllers can get to it."""
|
||||||
|
|
||||||
|
def before(self, state):
|
||||||
|
state.request.dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
|
|
||||||
|
class RPCHook(hooks.PecanHook):
|
||||||
|
"""Attach the rpcapi object to the request so controllers can get to it."""
|
||||||
|
|
||||||
|
def before(self, state):
|
||||||
|
state.request.rpcapi = rpcapi.ConductorAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class SystemConfigHook(hooks.PecanHook):
|
||||||
|
"""Attach the rpcapi object to the request so controllers can get to it."""
|
||||||
|
|
||||||
|
def before(self, state):
|
||||||
|
state.request.systemconfig = systemconfig_plugin.SystemConfigPlugin(
|
||||||
|
invoke_kwds={'context': state.request.context})
|
||||||
|
|
||||||
|
# state.request.systemconfig = systemconfig.SystemConfigOperator(
|
||||||
|
# state.request.context,
|
||||||
|
# state.request.dbapi)
|
|
@ -0,0 +1,19 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
from inventory.api.middleware import auth_token
|
||||||
|
from inventory.api.middleware import parsable_error
|
||||||
|
# from inventory.api.middleware import json_ext
|
||||||
|
|
||||||
|
|
||||||
|
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
|
||||||
|
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
|
||||||
|
# JsonExtensionMiddleware = json_ext.JsonExtensionMiddleware
|
||||||
|
|
||||||
|
__all__ = ('ParsableErrorMiddleware',
|
||||||
|
'AuthTokenMiddleware')
|
||||||
|
|
||||||
|
# 'JsonExtensionMiddleware')
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from keystonemiddleware import auth_token
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from inventory.common import exception
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import utils
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTokenMiddleware(auth_token.AuthProtocol):
|
||||||
|
"""A wrapper on Keystone auth_token middleware.
|
||||||
|
|
||||||
|
Does not perform verification of authentication tokens
|
||||||
|
for public routes in the API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, app, conf, public_api_routes=None):
|
||||||
|
if public_api_routes is None:
|
||||||
|
public_api_routes = []
|
||||||
|
route_pattern_tpl = '%s(\.json)?$'
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
|
||||||
|
for route_tpl in public_api_routes]
|
||||||
|
except re.error as e:
|
||||||
|
msg = _('Cannot compile public API routes: %s') % e
|
||||||
|
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.ConfigInvalid(error_msg=msg)
|
||||||
|
|
||||||
|
super(AuthTokenMiddleware, self).__init__(app, conf)
|
||||||
|
|
||||||
|
def __call__(self, env, start_response):
|
||||||
|
path = utils.safe_rstrip(env.get('PATH_INFO'), '/')
|
||||||
|
|
||||||
|
# The information whether the API call is being performed against the
|
||||||
|
# public API is required for some other components. Saving it to the
|
||||||
|
# WSGI environment is reasonable thereby.
|
||||||
|
env['is_public_api'] = any(map(lambda pattern: re.match(pattern, path),
|
||||||
|
self.public_api_routes))
|
||||||
|
|
||||||
|
if env['is_public_api']:
|
||||||
|
return self._app(env, start_response)
|
||||||
|
|
||||||
|
return super(AuthTokenMiddleware, self).__call__(env, start_response)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def factory(cls, global_config, **local_conf):
|
||||||
|
public_routes = local_conf.get('acl_public_routes', '')
|
||||||
|
public_api_routes = [path.strip() for path in public_routes.split(',')]
|
||||||
|
|
||||||
|
def _factory(app):
|
||||||
|
return cls(app, global_config, public_api_routes=public_api_routes)
|
||||||
|
return _factory
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""
|
||||||
|
Middleware to replace the plain text message body of an error
|
||||||
|
response with one formatted so the client can parse it.
|
||||||
|
|
||||||
|
Based on pecan.middleware.errordocument
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from xml import etree as et
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
import six
|
||||||
|
import webob
|
||||||
|
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ParsableErrorMiddleware(object):
|
||||||
|
"""Replace error body with something the client can parse."""
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
# Request for this state, modified by replace_start_response()
|
||||||
|
# and used when an error is being reported.
|
||||||
|
state = {}
|
||||||
|
|
||||||
|
def replacement_start_response(status, headers, exc_info=None):
|
||||||
|
"""Overrides the default response to make errors parsable."""
|
||||||
|
try:
|
||||||
|
status_code = int(status.split(' ')[0])
|
||||||
|
state['status_code'] = status_code
|
||||||
|
except (ValueError, TypeError): # pragma: nocover
|
||||||
|
raise Exception(_(
|
||||||
|
'ErrorDocumentMiddleware received an invalid '
|
||||||
|
'status %s') % status)
|
||||||
|
else:
|
||||||
|
if (state['status_code'] // 100) not in (2, 3):
|
||||||
|
# Remove some headers so we can replace them later
|
||||||
|
# when we have the full error message and can
|
||||||
|
# compute the length.
|
||||||
|
headers = [(h, v)
|
||||||
|
for (h, v) in headers
|
||||||
|
if h not in ('Content-Length', 'Content-Type')
|
||||||
|
]
|
||||||
|
# Save the headers in case we need to modify them.
|
||||||
|
state['headers'] = headers
|
||||||
|
return start_response(status, headers, exc_info)
|
||||||
|
|
||||||
|
# The default is application/json. However, Pecan will try
|
||||||
|
# to output HTML errors if no Accept header is provided.
|
||||||
|
if 'HTTP_ACCEPT' not in environ or environ['HTTP_ACCEPT'] == '*/*':
|
||||||
|
environ['HTTP_ACCEPT'] = 'application/json'
|
||||||
|
|
||||||
|
app_iter = self.app(environ, replacement_start_response)
|
||||||
|
if (state['status_code'] // 100) not in (2, 3):
|
||||||
|
req = webob.Request(environ)
|
||||||
|
if (req.accept.best_match(
|
||||||
|
['application/json', 'application/xml']) ==
|
||||||
|
'application/xml'):
|
||||||
|
try:
|
||||||
|
# simple check xml is valid
|
||||||
|
body = [et.ElementTree.tostring(
|
||||||
|
et.ElementTree.fromstring('<error_message>' +
|
||||||
|
'\n'.join(app_iter) +
|
||||||
|
'</error_message>'))]
|
||||||
|
except et.ElementTree.ParseError as err:
|
||||||
|
LOG.error('Error parsing HTTP response: %s', err)
|
||||||
|
body = ['<error_message>%s' % state['status_code'] +
|
||||||
|
'</error_message>']
|
||||||
|
state['headers'].append(('Content-Type', 'application/xml'))
|
||||||
|
else:
|
||||||
|
if six.PY3:
|
||||||
|
app_iter = [i.decode('utf-8') for i in app_iter]
|
||||||
|
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
|
||||||
|
if six.PY3:
|
||||||
|
body = [item.encode('utf-8') for item in body]
|
||||||
|
state['headers'].append(('Content-Type', 'application/json'))
|
||||||
|
state['headers'].append(('Content-Length', str(len(body[0]))))
|
||||||
|
else:
|
||||||
|
body = app_iter
|
||||||
|
return body
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.environ['EVENTLET_NO_GREENDNS'] = 'yes' # noqa E402
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
|
||||||
|
eventlet.monkey_patch(os=False)
|
||||||
|
|
||||||
|
import oslo_i18n as i18n # noqa I202
|
||||||
|
|
||||||
|
i18n.install('inventory')
|
|
@ -0,0 +1,58 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
#
|
||||||
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
The Inventory Agent Service
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_service import service
|
||||||
|
|
||||||
|
from inventory.common import rpc_service
|
||||||
|
from inventory.common import service as inventory_service
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Parse config file and command line options, then start logging
|
||||||
|
inventory_service.prepare_service(sys.argv)
|
||||||
|
|
||||||
|
# connection is based upon host and MANAGER_TOPIC
|
||||||
|
mgr = rpc_service.RPCService(CONF.host,
|
||||||
|
'inventory.agent.manager',
|
||||||
|
'AgentManager')
|
||||||
|
launcher = service.launch(CONF, mgr)
|
||||||
|
launcher.wait()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
|
@ -0,0 +1,86 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_service import systemd
|
||||||
|
from oslo_service import wsgi
|
||||||
|
|
||||||
|
import logging as std_logging
|
||||||
|
|
||||||
|
from inventory.api import app
|
||||||
|
from inventory.api import config
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
|
||||||
|
api_opts = [
|
||||||
|
cfg.StrOpt('bind_host',
|
||||||
|
default="0.0.0.0",
|
||||||
|
help=_('IP address for inventory api to listen')),
|
||||||
|
cfg.IntOpt('bind_port',
|
||||||
|
default=6380,
|
||||||
|
help=_('listen port for inventory api')),
|
||||||
|
cfg.StrOpt('bind_host_pxe',
|
||||||
|
default="0.0.0.0",
|
||||||
|
help=_('IP address for inventory api pxe to listen')),
|
||||||
|
cfg.IntOpt('api_workers', default=2,
|
||||||
|
help=_("number of api workers")),
|
||||||
|
cfg.IntOpt('limit_max',
|
||||||
|
default=1000,
|
||||||
|
help='the maximum number of items returned in a single '
|
||||||
|
'response from a collection resource')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
eventlet.monkey_patch(os=False)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
config.init(sys.argv[1:])
|
||||||
|
config.setup_logging()
|
||||||
|
|
||||||
|
application = app.load_paste_app()
|
||||||
|
|
||||||
|
CONF.register_opts(api_opts, 'api')
|
||||||
|
|
||||||
|
host = CONF.api.bind_host
|
||||||
|
port = CONF.api.bind_port
|
||||||
|
workers = CONF.api.api_workers
|
||||||
|
|
||||||
|
if workers < 1:
|
||||||
|
LOG.warning("Wrong worker number, worker = %(workers)s", workers)
|
||||||
|
workers = 1
|
||||||
|
|
||||||
|
LOG.info("Serving on http://%(host)s:%(port)s with %(workers)s",
|
||||||
|
{'host': host, 'port': port, 'workers': workers})
|
||||||
|
systemd.notify_once()
|
||||||
|
service = wsgi.Server(CONF, CONF.prog, application, host, port)
|
||||||
|
|
||||||
|
app.serve(service, CONF, workers)
|
||||||
|
|
||||||
|
pxe_host = CONF.api.bind_host_pxe
|
||||||
|
if pxe_host:
|
||||||
|
pxe_service = wsgi.Server(CONF, CONF.prog, application, pxe_host, port)
|
||||||
|
app.serve_pxe(pxe_service, CONF, 1)
|
||||||
|
|
||||||
|
LOG.debug("Configuration:")
|
||||||
|
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||||
|
|
||||||
|
app.wait()
|
||||||
|
if pxe_host:
|
||||||
|
app.wait_pxe()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
#
|
||||||
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
The Inventory Conductor Service
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_service import service
|
||||||
|
|
||||||
|
from inventory.common import rpc_service
|
||||||
|
from inventory.common import service as inventory_service
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Parse config file and command line options, then start logging
|
||||||
|
inventory_service.prepare_service(sys.argv)
|
||||||
|
|
||||||
|
mgr = rpc_service.RPCService(CONF.host,
|
||||||
|
'inventory.conductor.manager',
|
||||||
|
'ConductorManager')
|
||||||
|
|
||||||
|
launcher = service.launch(CONF, mgr)
|
||||||
|
launcher.wait()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
|
@ -0,0 +1,19 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from inventory.db import migration
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cfg.CONF(sys.argv[1:],
|
||||||
|
project='inventory')
|
||||||
|
migration.db_sync()
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
#
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2016 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handle lease database updates from dnsmasq DHCP server
|
||||||
|
This file was based on dhcpbridge.py from nova
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from inventory.common import context
|
||||||
|
from inventory.common.i18n import _
|
||||||
|
from inventory.common import service as inventory_service
|
||||||
|
from inventory.conductor import rpcapi as conductor_rpcapi
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def add_lease(mac, ip_address):
|
||||||
|
"""Called when a new lease is created."""
|
||||||
|
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
rpcapi = \
|
||||||
|
conductor_rpcapi.ConductorAPI(topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
|
||||||
|
cid = None
|
||||||
|
cid = os.getenv('DNSMASQ_CLIENT_ID')
|
||||||
|
|
||||||
|
tags = None
|
||||||
|
tags = os.getenv('DNSMASQ_TAGS')
|
||||||
|
|
||||||
|
if tags is not None:
|
||||||
|
# TODO(sc): Maybe this shouldn't be synchronous - if this hangs,
|
||||||
|
# we could cause dnsmasq to get stuck...
|
||||||
|
rpcapi.handle_dhcp_lease(ctxt, tags, mac, ip_address, cid)
|
||||||
|
|
||||||
|
|
||||||
|
def old_lease(mac, ip_address):
|
||||||
|
"""Called when an old lease is recognized."""
|
||||||
|
|
||||||
|
# This happens when a node is rebooted, but it can also happen if the
|
||||||
|
# node was deleted and then rebooted, so we need to re-add in that case.
|
||||||
|
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
rpcapi = conductor_rpcapi.ConductorAPI(
|
||||||
|
topic=conductor_rpcapi.MANAGER_TOPIC)
|
||||||
|
|
||||||
|
cid = None
|
||||||
|
cid = os.getenv('DNSMASQ_CLIENT_ID')
|
||||||
|
|
||||||
|
tags = None
|
||||||
|
tags = os.getenv('DNSMASQ_TAGS')
|
||||||
|
|
||||||
|
if tags is not None:
|
||||||
|
# TODO(sc): Maybe this shouldn't be synchronous - if this hangs,
|
||||||
|
# we could cause dnsmasq to get stuck...
|
||||||
|
rpcapi.handle_dhcp_lease(ctxt, tags, mac, ip_address, cid)
|
||||||
|
|
||||||
|
|
||||||
|
def del_lease(mac, ip_address):
|
||||||
|
"""Called when a lease expires."""
|
||||||
|
# We will only delete the ihost when it is requested by the user.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def add_action_parsers(subparsers):
|
||||||
|
# NOTE(cfb): dnsmasq always passes mac, and ip. hostname
|
||||||
|
# is passed if known. We don't care about
|
||||||
|
# hostname, but argparse will complain if we
|
||||||
|
# do not accept it.
|
||||||
|
for action in ['add', 'del', 'old']:
|
||||||
|
parser = subparsers.add_parser(action)
|
||||||
|
parser.add_argument('mac')
|
||||||
|
parser.add_argument('ip')
|
||||||
|
parser.add_argument('hostname', nargs='?', default='')
|
||||||
|
parser.set_defaults(func=globals()[action + '_lease'])
|
||||||
|
|
||||||
|
|
||||||
|
CONF.register_cli_opt(
|
||||||
|
cfg.SubCommandOpt('action',
|
||||||
|
title='Action options',
|
||||||
|
help='Available dnsmasq_lease_update options',
|
||||||
|
handler=add_action_parsers))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Parse config file and command line options, then start logging
|
||||||
|
# The mac is to be truncated to 17 characters, which is the proper
|
||||||
|
# length of a mac address, in order to handle IPv6 where a DUID
|
||||||
|
# is provided instead of a mac address. The truncated DUID is
|
||||||
|
# then equivalent to the mac address.
|
||||||
|
inventory_service.prepare_service(sys.argv)
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
if CONF.action.name in ['add', 'del', 'old']:
|
||||||
|
msg = (_("Called '%(action)s' for mac '%(mac)s' with ip '%(ip)s'") %
|
||||||
|
{"action": CONF.action.name,
|
||||||
|
"mac": CONF.action.mac[-17:],
|
||||||
|
"ip": CONF.action.ip})
|
||||||
|
LOG.info(msg)
|
||||||
|
CONF.action.func(CONF.action.mac[-17:], CONF.action.ip)
|
||||||
|
else:
|
||||||
|
LOG.error(_("Unknown action: %(action)") % {"action":
|
||||||
|
CONF.action.name})
|
|
@ -0,0 +1,43 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIResourceWrapper(object):
|
||||||
|
"""Simple wrapper for api objects.
|
||||||
|
|
||||||
|
Define _attrs on the child class and pass in the
|
||||||
|
api object as the only argument to the constructor
|
||||||
|
"""
|
||||||
|
_attrs = []
|
||||||
|
_apiresource = None # Make sure _apiresource is there even in __init__.
|
||||||
|
|
||||||
|
def __init__(self, apiresource):
|
||||||
|
self._apiresource = apiresource
|
||||||
|
|
||||||
|
def __getattribute__(self, attr):
|
||||||
|
try:
|
||||||
|
return object.__getattribute__(self, attr)
|
||||||
|
except AttributeError:
|
||||||
|
if attr not in self._attrs:
|
||||||
|
raise
|
||||||
|
# __getattr__ won't find properties
|
||||||
|
return getattr(self._apiresource, attr)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s: %s>" % (self.__class__.__name__,
|
||||||
|
dict((attr, getattr(self, attr))
|
||||||
|
for attr in self._attrs
|
||||||
|
if hasattr(self, attr)))
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
obj = {}
|
||||||
|
for key in self._attrs:
|
||||||
|
obj[key] = getattr(self._apiresource, key, None)
|
||||||
|
return obj
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue