Relocate ceph-manager to stx-integ/ceph/ceph-manager
Move content from stx-upstream to stx-integ Packages will be relocated to stx-integ: ceph/ ceph ceph-manager Change-Id: I129faa448e2e52fc82101ae7ebc8ad5688f21523 Story: 2002801 Task: 22687 Signed-off-by: Scott Little <scott.little@windriver.com>
This commit is contained in:
parent
d4a5366f53
commit
557dfe686c
6
ceph/ceph-manager/.gitignore
vendored
Normal file
6
ceph/ceph-manager/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
!.distro
|
||||
.distro/centos7/rpmbuild/RPMS
|
||||
.distro/centos7/rpmbuild/SRPMS
|
||||
.distro/centos7/rpmbuild/BUILD
|
||||
.distro/centos7/rpmbuild/BUILDROOT
|
||||
.distro/centos7/rpmbuild/SOURCES/ceph-manager*tar.gz
|
202
ceph/ceph-manager/LICENSE
Normal file
202
ceph/ceph-manager/LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
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.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
13
ceph/ceph-manager/PKG-INFO
Normal file
13
ceph/ceph-manager/PKG-INFO
Normal file
@ -0,0 +1,13 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: ceph-manager
|
||||
Version: 1.0
|
||||
Summary: Handle Ceph API calls and provide status updates via alarms
|
||||
Home-page:
|
||||
Author: Windriver
|
||||
Author-email: info@windriver.com
|
||||
License: Apache-2.0
|
||||
|
||||
Description: Handle Ceph API calls and provide status updates via alarms
|
||||
|
||||
|
||||
Platform: UNKNOWN
|
3
ceph/ceph-manager/centos/build_srpm.data
Normal file
3
ceph/ceph-manager/centos/build_srpm.data
Normal file
@ -0,0 +1,3 @@
|
||||
SRC_DIR="ceph-manager"
|
||||
COPY_LIST_TO_TAR="files scripts"
|
||||
TIS_PATCH_VER=4
|
70
ceph/ceph-manager/centos/ceph-manager.spec
Normal file
70
ceph/ceph-manager/centos/ceph-manager.spec
Normal file
@ -0,0 +1,70 @@
|
||||
Summary: Handle Ceph API calls and provide status updates via alarms
|
||||
Name: ceph-manager
|
||||
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: python-setuptools
|
||||
BuildRequires: systemd-units
|
||||
BuildRequires: systemd-devel
|
||||
Requires: sysinv
|
||||
|
||||
%description
|
||||
Handle Ceph API calls and provide status updates via alarms.
|
||||
Handle sysinv RPC calls for long running Ceph API operations:
|
||||
- cache tiering enable
|
||||
- cache tiering disable
|
||||
|
||||
%define local_bindir /usr/bin/
|
||||
%define local_etc_initd /etc/init.d/
|
||||
%define local_etc_logrotated /etc/logrotate.d/
|
||||
%define pythonroot /usr/lib64/python2.7/site-packages
|
||||
|
||||
%define debug_package %{nil}
|
||||
|
||||
%prep
|
||||
%setup
|
||||
|
||||
%build
|
||||
%{__python} setup.py build
|
||||
|
||||
%install
|
||||
%{__python} setup.py install --root=$RPM_BUILD_ROOT \
|
||||
--install-lib=%{pythonroot} \
|
||||
--prefix=/usr \
|
||||
--install-data=/usr/share \
|
||||
--single-version-externally-managed
|
||||
|
||||
install -d -m 755 %{buildroot}%{local_etc_initd}
|
||||
install -p -D -m 700 scripts/init.d/ceph-manager %{buildroot}%{local_etc_initd}/ceph-manager
|
||||
|
||||
install -d -m 755 %{buildroot}%{local_bindir}
|
||||
install -p -D -m 700 scripts/bin/ceph-manager %{buildroot}%{local_bindir}/ceph-manager
|
||||
|
||||
install -d -m 755 %{buildroot}%{local_etc_logrotated}
|
||||
install -p -D -m 644 files/ceph-manager.logrotate %{buildroot}%{local_etc_logrotated}/ceph-manager.logrotate
|
||||
|
||||
install -d -m 755 %{buildroot}%{_unitdir}
|
||||
install -m 644 -p -D files/%{name}.service %{buildroot}%{_unitdir}/%{name}.service
|
||||
|
||||
%clean
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
# Note: The package name is ceph-manager but the import name is ceph_manager so
|
||||
# can't use '%{name}'.
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%doc LICENSE
|
||||
%{local_bindir}/*
|
||||
%{local_etc_initd}/*
|
||||
%{_unitdir}/%{name}.service
|
||||
%dir %{local_etc_logrotated}
|
||||
%{local_etc_logrotated}/*
|
||||
%dir %{pythonroot}/ceph_manager
|
||||
%{pythonroot}/ceph_manager/*
|
||||
%dir %{pythonroot}/ceph_manager-%{version}.0-py2.7.egg-info
|
||||
%{pythonroot}/ceph_manager-%{version}.0-py2.7.egg-info/*
|
202
ceph/ceph-manager/ceph-manager/LICENSE
Normal file
202
ceph/ceph-manager/ceph-manager/LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
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.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
5
ceph/ceph-manager/ceph-manager/ceph_manager/__init__.py
Normal file
5
ceph/ceph-manager/ceph-manager/ceph_manager/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
#
|
||||
# Copyright (c) 2016 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
705
ceph/ceph-manager/ceph-manager/ceph_manager/cache_tiering.py
Normal file
705
ceph/ceph-manager/ceph-manager/ceph_manager/cache_tiering.py
Normal file
@ -0,0 +1,705 @@
|
||||
#
|
||||
# Copyright (c) 2016 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import copy
|
||||
import contextlib
|
||||
import functools
|
||||
import math
|
||||
import subprocess
|
||||
import time
|
||||
import traceback
|
||||
# noinspection PyUnresolvedReferences
|
||||
import eventlet
|
||||
# noinspection PyUnresolvedReferences
|
||||
from eventlet.semaphore import Semaphore
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_log import log as logging
|
||||
# noinspection PyUnresolvedReferences
|
||||
from sysinv.conductor.cache_tiering_service_config import ServiceConfig
|
||||
|
||||
from i18n import _LI, _LW, _LE
|
||||
|
||||
import constants
|
||||
import exception
|
||||
import ceph
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CEPH_POOLS = copy.deepcopy(constants.CEPH_POOLS)
|
||||
|
||||
MAX_WAIT = constants.CACHE_FLUSH_MAX_WAIT_OBJ_COUNT_DECREASE_SEC
|
||||
MIN_WAIT = constants.CACHE_FLUSH_MIN_WAIT_OBJ_COUNT_DECREASE_SEC
|
||||
|
||||
|
||||
class LockOwnership(object):
|
||||
def __init__(self, sem):
|
||||
self.sem = sem
|
||||
|
||||
@contextlib.contextmanager
|
||||
def __call__(self):
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if self.sem:
|
||||
self.sem.release()
|
||||
|
||||
def transfer(self):
|
||||
new_lo = LockOwnership(self.sem)
|
||||
self.sem = None
|
||||
return new_lo
|
||||
|
||||
|
||||
class Lock(object):
|
||||
|
||||
def __init__(self):
|
||||
self.sem = Semaphore(value=1)
|
||||
|
||||
def try_lock(self):
|
||||
result = self.sem.acquire(blocking=False)
|
||||
if result:
|
||||
return LockOwnership(self.sem)
|
||||
|
||||
|
||||
class CacheTiering(object):
|
||||
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
self.lock = Lock()
|
||||
# will be unlocked by set_initial_config()
|
||||
self._init_config_lock = self.lock.try_lock()
|
||||
self.config = None
|
||||
self.config_desired = None
|
||||
self.config_applied = None
|
||||
self.target_max_bytes = {}
|
||||
|
||||
def set_initial_config(self, config):
|
||||
with self._init_config_lock():
|
||||
LOG.info("Setting Ceph cache tiering initial configuration")
|
||||
self.config = ServiceConfig.from_dict(
|
||||
config.get(constants.CACHE_TIERING, {})) or \
|
||||
ServiceConfig()
|
||||
self.config_desired = ServiceConfig.from_dict(
|
||||
config.get(constants.CACHE_TIERING_DESIRED, {})) or \
|
||||
ServiceConfig()
|
||||
self.config_applied = ServiceConfig.from_dict(
|
||||
config.get(constants.CACHE_TIERING_APPLIED, {})) or \
|
||||
ServiceConfig()
|
||||
if self.config_desired:
|
||||
LOG.debug("set_initial_config config_desired %s " %
|
||||
self.config_desired.to_dict())
|
||||
if self.config_applied:
|
||||
LOG.debug("set_initial_config config_applied %s " %
|
||||
self.config_applied.to_dict())
|
||||
|
||||
# Check that previous caching tier operation completed
|
||||
# successfully or perform recovery
|
||||
if (self.config_desired and
|
||||
self.config_applied and
|
||||
(self.config_desired.cache_enabled !=
|
||||
self.config_applied.cache_enabled)):
|
||||
if self.config_desired.cache_enabled:
|
||||
self.enable_cache(self.config_desired.to_dict(),
|
||||
self.config_applied.to_dict(),
|
||||
self._init_config_lock.transfer())
|
||||
else:
|
||||
self.disable_cache(self.config_desired.to_dict(),
|
||||
self.config_applied.to_dict(),
|
||||
self._init_config_lock.transfer())
|
||||
|
||||
def is_locked(self):
|
||||
lock_ownership = self.lock.try_lock()
|
||||
if not lock_ownership:
|
||||
return True
|
||||
with lock_ownership():
|
||||
return False
|
||||
|
||||
def update_pools_info(self):
|
||||
global CEPH_POOLS
|
||||
cfg = self.service.sysinv_conductor.call(
|
||||
{}, 'get_ceph_pools_config')
|
||||
CEPH_POOLS = copy.deepcopy(cfg)
|
||||
LOG.info(_LI("update_pools_info: pools: {}").format(CEPH_POOLS))
|
||||
|
||||
def enable_cache(self, new_config, applied_config, lock_ownership=None):
|
||||
new_config = ServiceConfig.from_dict(new_config)
|
||||
applied_config = ServiceConfig.from_dict(applied_config)
|
||||
if not lock_ownership:
|
||||
lock_ownership = self.lock.try_lock()
|
||||
if not lock_ownership:
|
||||
raise exception.CephCacheEnableFailure()
|
||||
with lock_ownership():
|
||||
eventlet.spawn(self.do_enable_cache,
|
||||
new_config, applied_config,
|
||||
lock_ownership.transfer())
|
||||
|
||||
def do_enable_cache(self, new_config, applied_config, lock_ownership):
|
||||
LOG.info(_LI("cache_tiering_enable_cache: "
|
||||
"new_config={}, applied_config={}").format(
|
||||
new_config.to_dict(), applied_config.to_dict()))
|
||||
_unwind_actions = []
|
||||
with lock_ownership():
|
||||
success = False
|
||||
_exception = None
|
||||
try:
|
||||
self.config_desired.cache_enabled = True
|
||||
self.update_pools_info()
|
||||
for pool in CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = \
|
||||
self.service.monitor._get_object_pool_name()
|
||||
pool['pool_name'] = object_pool_name
|
||||
|
||||
self.cache_pool_create(pool)
|
||||
_unwind_actions.append(
|
||||
functools.partial(self.cache_pool_delete, pool))
|
||||
for pool in CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = \
|
||||
self.service.monitor._get_object_pool_name()
|
||||
pool['pool_name'] = object_pool_name
|
||||
|
||||
self.cache_tier_add(pool)
|
||||
_unwind_actions.append(
|
||||
functools.partial(self.cache_tier_remove, pool))
|
||||
for pool in CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = \
|
||||
self.service.monitor._get_object_pool_name()
|
||||
pool['pool_name'] = object_pool_name
|
||||
|
||||
self.cache_mode_set(pool, 'writeback')
|
||||
self.cache_pool_set_config(pool, new_config)
|
||||
self.cache_overlay_create(pool)
|
||||
success = True
|
||||
except Exception as e:
|
||||
LOG.error(_LE('Failed to enable cache: reason=%s') %
|
||||
traceback.format_exc())
|
||||
for action in reversed(_unwind_actions):
|
||||
try:
|
||||
action()
|
||||
except Exception:
|
||||
LOG.warn(_LW('Failed cache enable '
|
||||
'unwind action: reason=%s') %
|
||||
traceback.format_exc())
|
||||
success = False
|
||||
_exception = str(e)
|
||||
finally:
|
||||
self.service.monitor.monitor_check_cache_tier(success)
|
||||
if success:
|
||||
self.config_applied.cache_enabled = True
|
||||
self.service.sysinv_conductor.call(
|
||||
{}, 'cache_tiering_enable_cache_complete',
|
||||
success=success, exception=_exception,
|
||||
new_config=new_config.to_dict(),
|
||||
applied_config=applied_config.to_dict())
|
||||
# Run first update of periodic target_max_bytes
|
||||
self.update_cache_target_max_bytes()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ignore_ceph_failure(self):
|
||||
try:
|
||||
yield
|
||||
except exception.CephManagerException:
|
||||
pass
|
||||
|
||||
def disable_cache(self, new_config, applied_config, lock_ownership=None):
|
||||
new_config = ServiceConfig.from_dict(new_config)
|
||||
applied_config = ServiceConfig.from_dict(applied_config)
|
||||
if not lock_ownership:
|
||||
lock_ownership = self.lock.try_lock()
|
||||
if not lock_ownership:
|
||||
raise exception.CephCacheDisableFailure()
|
||||
with lock_ownership():
|
||||
eventlet.spawn(self.do_disable_cache,
|
||||
new_config, applied_config,
|
||||
lock_ownership.transfer())
|
||||
|
||||
def do_disable_cache(self, new_config, applied_config, lock_ownership):
|
||||
LOG.info(_LI("cache_tiering_disable_cache: "
|
||||
"new_config={}, applied_config={}").format(
|
||||
new_config, applied_config))
|
||||
with lock_ownership():
|
||||
success = False
|
||||
_exception = None
|
||||
try:
|
||||
self.config_desired.cache_enabled = False
|
||||
for pool in CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = \
|
||||
self.service.monitor._get_object_pool_name()
|
||||
pool['pool_name'] = object_pool_name
|
||||
|
||||
with self.ignore_ceph_failure():
|
||||
self.cache_mode_set(
|
||||
pool, 'forward')
|
||||
|
||||
for pool in CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = \
|
||||
self.service.monitor._get_object_pool_name()
|
||||
pool['pool_name'] = object_pool_name
|
||||
|
||||
retries_left = 3
|
||||
while True:
|
||||
try:
|
||||
self.cache_flush(pool)
|
||||
break
|
||||
except exception.CephCacheFlushFailure:
|
||||
retries_left -= 1
|
||||
if not retries_left:
|
||||
# give up
|
||||
break
|
||||
else:
|
||||
time.sleep(1)
|
||||
for pool in CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = \
|
||||
self.service.monitor._get_object_pool_name()
|
||||
pool['pool_name'] = object_pool_name
|
||||
|
||||
with self.ignore_ceph_failure():
|
||||
self.cache_overlay_delete(pool)
|
||||
self.cache_tier_remove(pool)
|
||||
for pool in CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = \
|
||||
self.service.monitor._get_object_pool_name()
|
||||
pool['pool_name'] = object_pool_name
|
||||
|
||||
with self.ignore_ceph_failure():
|
||||
self.cache_pool_delete(pool)
|
||||
success = True
|
||||
except Exception as e:
|
||||
LOG.warn(_LE('Failed to disable cache: reason=%s') %
|
||||
traceback.format_exc())
|
||||
_exception = str(e)
|
||||
finally:
|
||||
self.service.monitor.monitor_check_cache_tier(False)
|
||||
if success:
|
||||
self.config_desired.cache_enabled = False
|
||||
self.config_applied.cache_enabled = False
|
||||
self.service.sysinv_conductor.call(
|
||||
{}, 'cache_tiering_disable_cache_complete',
|
||||
success=success, exception=_exception,
|
||||
new_config=new_config.to_dict(),
|
||||
applied_config=applied_config.to_dict())
|
||||
|
||||
def get_pool_pg_num(self, pool_name):
|
||||
return self.service.sysinv_conductor.call(
|
||||
{}, 'get_pool_pg_num',
|
||||
pool_name=pool_name)
|
||||
|
||||
def cache_pool_create(self, pool):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
pg_num = self.get_pool_pg_num(cache_pool)
|
||||
if not ceph.osd_pool_exists(self.service.ceph_api, cache_pool):
|
||||
ceph.osd_pool_create(
|
||||
self.service.ceph_api, cache_pool,
|
||||
pg_num, pg_num)
|
||||
|
||||
def cache_pool_delete(self, pool):
|
||||
cache_pool = pool['pool_name'] + '-cache'
|
||||
ceph.osd_pool_delete(
|
||||
self.service.ceph_api, cache_pool)
|
||||
|
||||
def cache_tier_add(self, pool):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
response, body = self.service.ceph_api.osd_tier_add(
|
||||
backing_pool, cache_pool,
|
||||
force_nonempty="--force-nonempty",
|
||||
body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Added OSD tier: "
|
||||
"backing_pool={}, cache_pool={}").format(
|
||||
backing_pool, cache_pool))
|
||||
else:
|
||||
e = exception.CephPoolAddTierFailure(
|
||||
backing_pool=backing_pool,
|
||||
cache_pool=cache_pool,
|
||||
response_status_code=response.status_code,
|
||||
response_reason=response.reason,
|
||||
status=body.get('status'),
|
||||
output=body.get('output'))
|
||||
LOG.warn(e)
|
||||
raise e
|
||||
|
||||
def cache_tier_remove(self, pool):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
response, body = self.service.ceph_api.osd_tier_remove(
|
||||
backing_pool, cache_pool, body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Removed OSD tier: "
|
||||
"backing_pool={}, cache_pool={}").format(
|
||||
backing_pool, cache_pool))
|
||||
else:
|
||||
e = exception.CephPoolRemoveTierFailure(
|
||||
backing_pool=backing_pool,
|
||||
cache_pool=cache_pool,
|
||||
response_status_code=response.status_code,
|
||||
response_reason=response.reason,
|
||||
status=body.get('status'),
|
||||
output=body.get('output'))
|
||||
LOG.warn(e)
|
||||
raise e
|
||||
|
||||
def cache_mode_set(self, pool, mode):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
response, body = self.service.ceph_api.osd_tier_cachemode(
|
||||
cache_pool, mode, body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Set OSD tier cache mode: "
|
||||
"cache_pool={}, mode={}").format(cache_pool, mode))
|
||||
else:
|
||||
e = exception.CephCacheSetModeFailure(
|
||||
cache_pool=cache_pool,
|
||||
mode=mode,
|
||||
response_status_code=response.status_code,
|
||||
response_reason=response.reason,
|
||||
status=body.get('status'),
|
||||
output=body.get('output'))
|
||||
LOG.warn(e)
|
||||
raise e
|
||||
|
||||
def cache_pool_set_config(self, pool, config):
|
||||
for name, value in config.params.iteritems():
|
||||
self.cache_pool_set_param(pool, name, value)
|
||||
|
||||
def cache_pool_set_param(self, pool, name, value):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
ceph.osd_set_pool_param(
|
||||
self.service.ceph_api, cache_pool, name, value)
|
||||
|
||||
def cache_overlay_create(self, pool):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
response, body = self.service.ceph_api.osd_tier_set_overlay(
|
||||
backing_pool, cache_pool, body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Set OSD tier overlay: "
|
||||
"backing_pool={}, cache_pool={}").format(
|
||||
backing_pool, cache_pool))
|
||||
else:
|
||||
e = exception.CephCacheCreateOverlayFailure(
|
||||
backing_pool=backing_pool,
|
||||
cache_pool=cache_pool,
|
||||
response_status_code=response.status_code,
|
||||
response_reason=response.reason,
|
||||
status=body.get('status'),
|
||||
output=body.get('output'))
|
||||
LOG.warn(e)
|
||||
raise e
|
||||
|
||||
def cache_overlay_delete(self, pool):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = pool['pool_name']
|
||||
response, body = self.service.ceph_api.osd_tier_remove_overlay(
|
||||
backing_pool, body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Removed OSD tier overlay: "
|
||||
"backing_pool={}").format(backing_pool))
|
||||
else:
|
||||
e = exception.CephCacheDeleteOverlayFailure(
|
||||
backing_pool=backing_pool,
|
||||
cache_pool=cache_pool,
|
||||
response_status_code=response.status_code,
|
||||
response_reason=response.reason,
|
||||
status=body.get('status'),
|
||||
output=body.get('output'))
|
||||
LOG.warn(e)
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def rados_cache_flush_evict_all(pool):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
try:
|
||||
subprocess.check_call(
|
||||
['/usr/bin/rados', '-p', cache_pool, 'cache-flush-evict-all'])
|
||||
LOG.info(_LI("Flushed OSD cache pool:"
|
||||
"cache_pool={}").format(cache_pool))
|
||||
except subprocess.CalledProcessError as e:
|
||||
_e = exception.CephCacheFlushFailure(
|
||||
cache_pool=cache_pool,
|
||||
return_code=str(e.returncode),
|
||||
cmd=" ".join(e.cmd),
|
||||
output=e.output)
|
||||
LOG.warn(_e)
|
||||
raise _e
|
||||
|
||||
def cache_flush(self, pool):
|
||||
backing_pool = pool['pool_name']
|
||||
cache_pool = backing_pool + '-cache'
|
||||
try:
|
||||
# set target_max_objects to a small value to force evacuation of
|
||||
# objects from cache before we use rados cache-flush-evict-all
|
||||
# WARNING: assuming cache_pool will be deleted after flush so
|
||||
# we don't have to save/restore the value of target_max_objects
|
||||
#
|
||||
self.cache_pool_set_param(pool, 'target_max_objects', 1)
|
||||
prev_object_count = None
|
||||
wait_interval = MIN_WAIT
|
||||
while True:
|
||||
response, body = self.service.ceph_api.df(body='json')
|
||||
if not response.ok:
|
||||
LOG.warn(_LW(
|
||||
"Failed to retrieve cluster free space stats: "
|
||||
"status_code=%d, reason=%s") % (
|
||||
response.status_code, response.reason))
|
||||
break
|
||||
stats = None
|
||||
for s in body['output']['pools']:
|
||||
if s['name'] == cache_pool:
|
||||
stats = s['stats']
|
||||
break
|
||||
if not stats:
|
||||
LOG.warn(_LW("Missing pool free space stats: "
|
||||
"cache_pool=%s") % cache_pool)
|
||||
break
|
||||
object_count = stats['objects']
|
||||
if object_count < constants.CACHE_FLUSH_OBJECTS_THRESHOLD:
|
||||
break
|
||||
if prev_object_count is not None:
|
||||
delta_objects = object_count - prev_object_count
|
||||
if delta_objects > 0:
|
||||
LOG.warn(_LW("Unexpected increase in number "
|
||||
"of objects in cache pool: "
|
||||
"cache_pool=%s, prev_object_count=%d, "
|
||||
"object_count=%d") % (
|
||||
cache_pool, prev_object_count,
|
||||
object_count))
|
||||
break
|
||||
if delta_objects == 0:
|
||||
wait_interval *= 2
|
||||
if wait_interval > MAX_WAIT:
|
||||
LOG.warn(_LW(
|
||||
"Cache pool number of objects did not "
|
||||
"decrease: cache_pool=%s, object_count=%d, "
|
||||
"wait_interval=%d") % (
|
||||
cache_pool, object_count, wait_interval))
|
||||
break
|
||||
else:
|
||||
wait_interval = MIN_WAIT
|
||||
time.sleep(wait_interval)
|
||||
prev_object_count = object_count
|
||||
except exception.CephPoolSetParamFailure as e:
|
||||
LOG.warn(e)
|
||||
finally:
|
||||
self.rados_cache_flush_evict_all(pool)
|
||||
|
||||
def update_cache_target_max_bytes(self):
|
||||
"Dynamically compute target_max_bytes of caching pools"
|
||||
|
||||
# Only compute if cache tiering is enabled
|
||||
if self.config_applied and self.config_desired:
|
||||
if (not self.config_desired.cache_enabled or
|
||||
not self.config_applied.cache_enabled):
|
||||
LOG.debug("Cache tiering disabled, no need to update "
|
||||
"target_max_bytes.")
|
||||
return
|
||||
LOG.debug("Updating target_max_bytes")
|
||||
|
||||
# Get available space
|
||||
response, body = self.service.ceph_api.osd_df(body='json',
|
||||
output_method='tree')
|
||||
if not response.ok:
|
||||
LOG.warn(_LW(
|
||||
"Failed to retrieve cluster free space stats: "
|
||||
"status_code=%d, reason=%s") % (
|
||||
response.status_code, response.reason))
|
||||
return
|
||||
|
||||
storage_tier_size = 0
|
||||
cache_tier_size = 0
|
||||
|
||||
replication = constants.CEPH_REPLICATION_FACTOR
|
||||
for node in body['output']['nodes']:
|
||||
if node['name'] == 'storage-tier':
|
||||
storage_tier_size = node['kb']*1024/replication
|
||||
elif node['name'] == 'cache-tier':
|
||||
cache_tier_size = node['kb']*1024/replication
|
||||
|
||||
if storage_tier_size == 0 or cache_tier_size == 0:
|
||||
LOG.info("Failed to get cluster size "
|
||||
"(storage_tier_size=%s, cache_tier_size=%s),"
|
||||
"retrying on next cycle" %
|
||||
(storage_tier_size, cache_tier_size))
|
||||
return
|
||||
|
||||
# Get available pools
|
||||
response, body = self.service.ceph_api.osd_lspools(body='json')
|
||||
if not response.ok:
|
||||
LOG.warn(_LW(
|
||||
"Failed to retrieve available pools: "
|
||||
"status_code=%d, reason=%s") % (
|
||||
response.status_code, response.reason))
|
||||
return
|
||||
pools = [p['poolname'] for p in body['output']]
|
||||
|
||||
# Separate backing from caching for easy iteration
|
||||
backing_pools = []
|
||||
caching_pools = []
|
||||
for p in pools:
|
||||
if p.endswith('-cache'):
|
||||
caching_pools.append(p)
|
||||
else:
|
||||
backing_pools.append(p)
|
||||
LOG.debug("Pools: caching: %s, backing: %s" % (caching_pools,
|
||||
backing_pools))
|
||||
|
||||
if not len(caching_pools):
|
||||
# We do not have caching pools created yet
|
||||
return
|
||||
|
||||
# Get quota from backing pools that are cached
|
||||
stats = {}
|
||||
for p in caching_pools:
|
||||
backing_name = p.replace('-cache', '')
|
||||
stats[backing_name] = {}
|
||||
try:
|
||||
quota = ceph.osd_pool_get_quota(self.service.ceph_api,
|
||||
backing_name)
|
||||
except exception.CephPoolGetQuotaFailure as e:
|
||||
LOG.warn(_LW(
|
||||
"Failed to retrieve quota: "
|
||||
"exception: %s") % str(e))
|
||||
return
|
||||
stats[backing_name]['quota'] = quota['max_bytes']
|
||||
stats[backing_name]['quota_pt'] = (quota['max_bytes']*100.0 /
|
||||
storage_tier_size)
|
||||
LOG.debug("Quota for pool: %s "
|
||||
"is: %s B representing %s pt" %
|
||||
(backing_name,
|
||||
quota['max_bytes'],
|
||||
stats[backing_name]['quota_pt']))
|
||||
|
||||
# target_max_bytes logic:
|
||||
# - For computing target_max_bytes cache_tier_size must be equal than
|
||||
# the sum of target_max_bytes of each caching pool
|
||||
# - target_max_bytes for each caching pool is computed as the
|
||||
# percentage of quota in corresponding backing pool
|
||||
# - the caching tiers has to work at full capacity, so if the sum of
|
||||
# all quotas in the backing tier is different than 100% we need to
|
||||
# normalize
|
||||
# - if the quota is zero for any pool we add CACHE_TIERING_MIN_QUOTA
|
||||
# by default *after* normalization so that we have real minimum
|
||||
|
||||
# We compute the real percentage that need to be normalized after
|
||||
# ensuring that we have CACHE_TIERING_MIN_QUOTA for each pool with
|
||||
# a quota of 0
|
||||
real_100pt = 90.0 # we start from max and decrease it for each 0 pool
|
||||
# Note: We must avoid reaching 100% at all costs! and
|
||||
# cache_target_full_ratio, the Ceph parameter that is supposed to
|
||||
# protect the cluster against this does not work in Ceph v0.94.6!
|
||||
# Therefore a value of 90% is better suited for this
|
||||
for p in caching_pools:
|
||||
backing_name = p.replace('-cache', '')
|
||||
if stats[backing_name]['quota_pt'] == 0:
|
||||
real_100pt -= constants.CACHE_TIERING_MIN_QUOTA
|
||||
LOG.debug("Quota before normalization for %s is: %s pt" %
|
||||
(p, stats[backing_name]['quota_pt']))
|
||||
|
||||
# Compute total percentage of quotas for all backing pools.
|
||||
# Should be 100% if correctly configured
|
||||
total_quota_pt = 0
|
||||
for p in caching_pools:
|
||||
backing_name = p.replace('-cache', '')
|
||||
total_quota_pt += stats[backing_name]['quota_pt']
|
||||
LOG.debug("Total quota pt is: %s" % total_quota_pt)
|
||||
|
||||
# Normalize quota pt to 100% (or real_100pt)
|
||||
if total_quota_pt != 0: # to avoid divide by zero
|
||||
for p in caching_pools:
|
||||
backing_name = p.replace('-cache', '')
|
||||
stats[backing_name]['quota_pt'] = \
|
||||
(stats[backing_name]['quota_pt'] *
|
||||
(real_100pt / total_quota_pt))
|
||||
|
||||
# Do not allow quota to be 0 for any pool
|
||||
total = 0
|
||||
for p in caching_pools:
|
||||
backing_name = p.replace('-cache', '')
|
||||
if stats[backing_name]['quota_pt'] == 0:
|
||||
stats[backing_name]['quota_pt'] = \
|
||||
constants.CACHE_TIERING_MIN_QUOTA
|
||||
total += stats[backing_name]['quota_pt']
|
||||
LOG.debug("Quota after normalization for %s is: %s:" %
|
||||
(p, stats[backing_name]['quota_pt']))
|
||||
|
||||
if total > 100:
|
||||
# Supplementary protection, we really have to avoid going above
|
||||
# 100%. Note that real_100pt is less than 100% but we still got
|
||||
# more than 100!
|
||||
LOG.warn("Total sum of quotas should not go above 100% "
|
||||
"but is: %s, recalculating in next cycle" % total)
|
||||
return
|
||||
LOG.debug("Total sum of quotas is %s pt" % total)
|
||||
|
||||
# Get current target_max_bytes. We cache it to reduce requests
|
||||
# to ceph-rest-api. We are the ones changing it, so not an issue.
|
||||
for p in caching_pools:
|
||||
if p not in self.target_max_bytes:
|
||||
try:
|
||||
value = ceph.osd_get_pool_param(self.service.ceph_api, p,
|
||||
constants.TARGET_MAX_BYTES)
|
||||
except exception.CephPoolGetParamFailure as e:
|
||||
LOG.warn(e)
|
||||
return
|
||||
self.target_max_bytes[p] = value
|
||||
LOG.debug("Existing target_max_bytes got from "
|
||||
"Ceph: %s" % self.target_max_bytes)
|
||||
|
||||
# Set TARGET_MAX_BYTES
|
||||
LOG.debug("storage_tier_size: %s "
|
||||
"cache_tier_size: %s" % (storage_tier_size,
|
||||
cache_tier_size))
|
||||
for p in caching_pools:
|
||||
backing_name = p.replace('-cache', '')
|
||||
s = stats[backing_name]
|
||||
target_max_bytes = math.floor(s['quota_pt'] * cache_tier_size /
|
||||
100.0)
|
||||
target_max_bytes = int(target_max_bytes)
|
||||
LOG.debug("New Target max bytes of pool: %s is: %s B" % (
|
||||
p, target_max_bytes))
|
||||
|
||||
# Set the new target_max_bytes only if it changed
|
||||
if self.target_max_bytes.get(p) == target_max_bytes:
|
||||
LOG.debug("Target max bytes of pool: %s "
|
||||
"is already updated" % p)
|
||||
continue
|
||||
try:
|
||||
ceph.osd_set_pool_param(self.service.ceph_api, p,
|
||||
constants.TARGET_MAX_BYTES,
|
||||
target_max_bytes)
|
||||
self.target_max_bytes[p] = target_max_bytes
|
||||
except exception.CephPoolSetParamFailure as e:
|
||||
LOG.warn(e)
|
||||
continue
|
||||
return
|
164
ceph/ceph-manager/ceph-manager/ceph_manager/ceph.py
Normal file
164
ceph/ceph-manager/ceph-manager/ceph_manager/ceph.py
Normal file
@ -0,0 +1,164 @@
|
||||
#
|
||||
# Copyright (c) 2016-2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import exception
|
||||
from i18n import _LI
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_log import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def osd_pool_set_quota(ceph_api, pool_name, max_bytes=0, max_objects=0):
|
||||
"""Set the quota for an OSD pool_name
|
||||
Setting max_bytes or max_objects to 0 will disable that quota param
|
||||
:param pool_name: OSD pool_name
|
||||
:param max_bytes: maximum bytes for OSD pool_name
|
||||
:param max_objects: maximum objects for OSD pool_name
|
||||
"""
|
||||
|
||||
# Update quota if needed
|
||||
prev_quota = osd_pool_get_quota(ceph_api, pool_name)
|
||||
if prev_quota["max_bytes"] != max_bytes:
|
||||
resp, b = ceph_api.osd_set_pool_quota(pool_name, 'max_bytes',
|
||||
max_bytes, body='json')
|
||||
if resp.ok:
|
||||
LOG.info(_LI("Set OSD pool_name quota: "
|
||||
"pool_name={}, max_bytes={}").format(
|
||||
pool_name, max_bytes))
|
||||
else:
|
||||
e = exception.CephPoolSetQuotaFailure(
|
||||
pool=pool_name, name='max_bytes',
|
||||
value=max_bytes, reason=resp.reason)
|
||||
LOG.error(e)
|
||||
raise e
|
||||
if prev_quota["max_objects"] != max_objects:
|
||||
resp, b = ceph_api.osd_set_pool_quota(pool_name, 'max_objects',
|
||||
max_objects,
|
||||
body='json')
|
||||
if resp.ok:
|
||||
LOG.info(_LI("Set OSD pool_name quota: "
|
||||
"pool_name={}, max_objects={}").format(
|
||||
pool_name, max_objects))
|
||||
else:
|
||||
e = exception.CephPoolSetQuotaFailure(
|
||||
pool=pool_name, name='max_objects',
|
||||
value=max_objects, reason=resp.reason)
|
||||
LOG.error(e)
|
||||
raise e
|
||||
|
||||
|
||||
def osd_pool_get_quota(ceph_api, pool_name):
|
||||
resp, quota = ceph_api.osd_get_pool_quota(pool_name, body='json')
|
||||
if not resp.ok:
|
||||
e = exception.CephPoolGetQuotaFailure(
|
||||
pool=pool_name, reason=resp.reason)
|
||||
LOG.error(e)
|
||||
raise e
|
||||
else:
|
||||
return {"max_objects": quota["output"]["quota_max_objects"],
|
||||
"max_bytes": quota["output"]["quota_max_bytes"]}
|
||||
|
||||
|
||||
def osd_pool_exists(ceph_api, pool_name):
|
||||
response, body = ceph_api.osd_pool_get(
|
||||
pool_name, "pg_num", body='json')
|
||||
if response.ok:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def osd_pool_create(ceph_api, pool_name, pg_num, pgp_num):
|
||||
if pool_name.endswith("-cache"):
|
||||
# ruleset 1: is the ruleset for the cache tier
|
||||
# Name: cache_tier_ruleset
|
||||
ruleset = 1
|
||||
else:
|
||||
# ruleset 0: is the default ruleset if no crushmap is loaded or
|
||||
# the ruleset for the backing tier if loaded:
|
||||
# Name: storage_tier_ruleset
|
||||
ruleset = 0
|
||||
response, body = ceph_api.osd_pool_create(
|
||||
pool_name, pg_num, pgp_num, pool_type="replicated",
|
||||
ruleset=ruleset, body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Created OSD pool: "
|
||||
"pool_name={}, pg_num={}, pgp_num={}, "
|
||||
"pool_type=replicated, ruleset={}").format(
|
||||
pool_name, pg_num, pgp_num, ruleset))
|
||||
else:
|
||||
e = exception.CephPoolCreateFailure(
|
||||
name=pool_name, reason=response.reason)
|
||||
LOG.error(e)
|
||||
raise e
|
||||
|
||||
# Explicitly assign the ruleset to the pool on creation since it is
|
||||
# ignored in the create call
|
||||
response, body = ceph_api.osd_set_pool_param(
|
||||
pool_name, "crush_ruleset", ruleset, body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Assigned crush ruleset to OS pool: "
|
||||
"pool_name={}, ruleset={}").format(
|
||||
pool_name, ruleset))
|
||||
else:
|
||||
e = exception.CephPoolRulesetFailure(
|
||||
name=pool_name, reason=response.reason)
|
||||
LOG.error(e)
|
||||
ceph_api.osd_pool_delete(
|
||||
pool_name, pool_name,
|
||||
sure='--yes-i-really-really-mean-it',
|
||||
body='json')
|
||||
raise e
|
||||
|
||||
|
||||
def osd_pool_delete(ceph_api, pool_name):
|
||||
"""Delete an osd pool
|
||||
:param pool_name: pool name
|
||||
"""
|
||||
response, body = ceph_api.osd_pool_delete(
|
||||
pool_name, pool_name,
|
||||
sure='--yes-i-really-really-mean-it',
|
||||
body='json')
|
||||
if response.ok:
|
||||
LOG.info(_LI("Deleted OSD pool {}").format(pool_name))
|
||||
else:
|
||||
e = exception.CephPoolDeleteFailure(
|
||||
name=pool_name, reason=response.reason)
|
||||
LOG.warn(e)
|
||||
raise e
|
||||
|
||||
|
||||
def osd_set_pool_param(ceph_api, pool_name, param, value):
|
||||
response, body = ceph_api.osd_set_pool_param(
|
||||
pool_name, param, value,
|
||||
force=None, body='json')
|
||||
if response.ok:
|
||||
LOG.info('OSD set pool param: '
|
||||
'pool={}, name={}, value={}'.format(
|
||||
pool_name, param, value))
|
||||
else:
|
||||
raise exception.CephPoolSetParamFailure(
|
||||
pool_name=pool_name,
|
||||
param=param,
|
||||
value=str(value),
|
||||
reason=response.reason)
|
||||
return response, body
|
||||
|
||||
|
||||
def osd_get_pool_param(ceph_api, pool_name, param):
|
||||
response, body = ceph_api.osd_get_pool_param(
|
||||
pool_name, param, body='json')
|
||||
if response.ok:
|
||||
LOG.debug('OSD get pool param: '
|
||||
'pool={}, name={}, value={}'.format(
|
||||
pool_name, param, body['output'][param]))
|
||||
else:
|
||||
raise exception.CephPoolGetParamFailure(
|
||||
pool_name=pool_name,
|
||||
param=param,
|
||||
reason=response.reason)
|
||||
return body['output'][param]
|
107
ceph/ceph-manager/ceph-manager/ceph_manager/constants.py
Normal file
107
ceph/ceph-manager/ceph-manager/ceph_manager/constants.py
Normal file
@ -0,0 +1,107 @@
|
||||
#
|
||||
# Copyright (c) 2016-2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
from i18n import _
|
||||
# noinspection PyUnresolvedReferences
|
||||
from sysinv.common import constants as sysinv_constants
|
||||
|
||||
CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL = \
|
||||
sysinv_constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL
|
||||
CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER = \
|
||||
sysinv_constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER
|
||||
CEPH_POOLS = sysinv_constants.BACKING_POOLS
|
||||
CEPH_REPLICATION_FACTOR = sysinv_constants.CEPH_REPLICATION_FACTOR_DEFAULT
|
||||
SERVICE_PARAM_CEPH_CACHE_HIT_SET_TYPE_BLOOM = \
|
||||
sysinv_constants.SERVICE_PARAM_CEPH_CACHE_HIT_SET_TYPE_BLOOM
|
||||
CACHE_TIERING_DEFAULTS = sysinv_constants.CACHE_TIERING_DEFAULTS
|
||||
TARGET_MAX_BYTES = \
|
||||
sysinv_constants.SERVICE_PARAM_CEPH_CACHE_TIER_TARGET_MAX_BYTES
|
||||
|
||||
# Cache tiering section shortener
|
||||
CACHE_TIERING = \
|
||||
sysinv_constants.SERVICE_PARAM_SECTION_CEPH_CACHE_TIER
|
||||
CACHE_TIERING_DESIRED = \
|
||||
sysinv_constants.SERVICE_PARAM_SECTION_CEPH_CACHE_TIER_DESIRED
|
||||
CACHE_TIERING_APPLIED = \
|
||||
sysinv_constants.SERVICE_PARAM_SECTION_CEPH_CACHE_TIER_APPLIED
|
||||
CACHE_TIERING_SECTIONS = \
|
||||
[CACHE_TIERING, CACHE_TIERING_DESIRED, CACHE_TIERING_APPLIED]
|
||||
|
||||
# Cache flush parameters
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD = 1000
|
||||
CACHE_FLUSH_MIN_WAIT_OBJ_COUNT_DECREASE_SEC = 1
|
||||
CACHE_FLUSH_MAX_WAIT_OBJ_COUNT_DECREASE_SEC = 128
|
||||
|
||||
CACHE_TIERING_MIN_QUOTA = 5
|
||||
|
||||
FM_ALARM_REASON_MAX_SIZE = 256
|
||||
|
||||
# TODO this will later change based on parsed health
|
||||
# clock skew is vm malfunction, mon or osd is equipment mal
|
||||
ALARM_CAUSE = 'equipment-malfunction'
|
||||
ALARM_TYPE = 'equipment'
|
||||
|
||||
# Ceph health check interval (in seconds)
|
||||
CEPH_HEALTH_CHECK_INTERVAL = 60
|
||||
|
||||
# Ceph health statuses
|
||||
CEPH_HEALTH_OK = 'HEALTH_OK'
|
||||
CEPH_HEALTH_WARN = 'HEALTH_WARN'
|
||||
CEPH_HEALTH_ERR = 'HEALTH_ERR'
|
||||
CEPH_HEALTH_DOWN = 'CEPH_DOWN'
|
||||
|
||||
# Statuses not reported by Ceph
|
||||
CEPH_STATUS_CUSTOM = [CEPH_HEALTH_DOWN]
|
||||
|
||||
SEVERITY = {CEPH_HEALTH_DOWN: 'critical',
|
||||
CEPH_HEALTH_ERR: 'critical',
|
||||
CEPH_HEALTH_WARN: 'warning'}
|
||||
|
||||
SERVICE_AFFECTING = {CEPH_HEALTH_DOWN: True,
|
||||
CEPH_HEALTH_ERR: True,
|
||||
CEPH_HEALTH_WARN: False}
|
||||
|
||||
# TODO this will later change based on parsed health
|
||||
ALARM_REASON_NO_OSD = _('no OSDs')
|
||||
ALARM_REASON_OSDS_DOWN = _('OSDs are down')
|
||||
ALARM_REASON_OSDS_OUT = _('OSDs are out')
|
||||
ALARM_REASON_OSDS_DOWN_OUT = _('OSDs are down/out')
|
||||
ALARM_REASON_PEER_HOST_DOWN = _('peer host down')
|
||||
|
||||
REPAIR_ACTION_MAJOR_CRITICAL_ALARM = _(
|
||||
'Ensure storage hosts from replication group are unlocked and available.'
|
||||
'Check if OSDs of each storage host are up and running.'
|
||||
'If problem persists, contact next level of support.')
|
||||
REPAIR_ACTION = _('If problem persists, contact next level of support.')
|
||||
|
||||
SYSINV_CONDUCTOR_TOPIC = 'sysinv.conductor_manager'
|
||||
CEPH_MANAGER_TOPIC = 'sysinv.ceph_manager'
|
||||
SYSINV_CONFIG_FILE = '/etc/sysinv/sysinv.conf'
|
||||
|
||||
# Titanium Cloud version strings
|
||||
TITANIUM_SERVER_VERSION_16_10 = '16.10'
|
||||
|
||||
CEPH_HEALTH_WARN_REQUIRE_JEWEL_OSDS_NOT_SET = (
|
||||
"all OSDs are running jewel or later but the "
|
||||
"'require_jewel_osds' osdmap flag is not set")
|
||||
|
||||
UPGRADE_COMPLETED = \
|
||||
sysinv_constants.UPGRADE_COMPLETED
|
||||
UPGRADE_ABORTING = \
|
||||
sysinv_constants.UPGRADE_ABORTING
|
||||
UPGRADE_ABORT_COMPLETING = \
|
||||
sysinv_constants.UPGRADE_ABORT_COMPLETING
|
||||
UPGRADE_ABORTING_ROLLBACK = \
|
||||
sysinv_constants.UPGRADE_ABORTING_ROLLBACK
|
||||
|
||||
CEPH_FLAG_REQUIRE_JEWEL_OSDS = 'require_jewel_osds'
|
||||
|
||||
# Tiers
|
||||
CEPH_CRUSH_TIER_SUFFIX = sysinv_constants.CEPH_CRUSH_TIER_SUFFIX
|
||||
SB_TIER_TYPE_CEPH = sysinv_constants.SB_TIER_TYPE_CEPH
|
||||
SB_TIER_SUPPORTED = sysinv_constants.SB_TIER_SUPPORTED
|
||||
SB_TIER_DEFAULT_NAMES = sysinv_constants.SB_TIER_DEFAULT_NAMES
|
||||
SB_TIER_CEPH_POOLS = sysinv_constants.SB_TIER_CEPH_POOLS
|
130
ceph/ceph-manager/ceph-manager/ceph_manager/exception.py
Normal file
130
ceph/ceph-manager/ceph-manager/ceph_manager/exception.py
Normal file
@ -0,0 +1,130 @@
|
||||
#
|
||||
# Copyright (c) 2016-2017 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from i18n import _, _LW
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_log import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CephManagerException(Exception):
|
||||
message = _("An unknown exception occurred.")
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
if not message:
|
||||
try:
|
||||
message = self.message % kwargs
|
||||
except TypeError:
|
||||
LOG.warn(_LW('Exception in string format operation'))
|
||||
for name, value in kwargs.iteritems():
|
||||
LOG.error("%s: %s" % (name, value))
|
||||
# at least get the core message out if something happened
|
||||
message = self.message
|
||||
super(CephManagerException, self).__init__(message)
|
||||
|
||||
|
||||
class CephPoolSetQuotaFailure(CephManagerException):
|
||||
message = _("Error seting the OSD pool "
|
||||
"quota %(name)s for %(pool)s to %(value)s") \
|
||||
+ ": %(reason)s"
|
||||
|
||||
|
||||
class CephPoolGetQuotaFailure(CephManagerException):
|
||||
message = _("Error geting the OSD pool quota for %(pool)s") \
|
||||
+ ": %(reason)s"
|
||||
|
||||
|
||||
class CephPoolCreateFailure(CephManagerException):
|
||||
message = _("Creating OSD pool %(name)s failed: %(reason)s")
|
||||
|
||||
|
||||
class CephPoolDeleteFailure(CephManagerException):
|
||||
message = _("Deleting OSD pool %(name)s failed: %(reason)s")
|
||||
|
||||
|
||||
class CephPoolRulesetFailure(CephManagerException):
|
||||
message = _("Assigning crush ruleset to OSD "
|
||||
"pool %(name)s failed: %(reason)s")
|
||||
|
||||
|
||||
class CephPoolAddTierFailure(CephManagerException):
|
||||
message = _("Failed to add OSD tier: "
|
||||
"backing_pool=%(backing_pool)s, cache_pool=%(cache_pool)s, "
|
||||
"response=%(response_status_code)s:%(response_reason)s, "
|
||||
"status=%(status)s, output=%(output)s")
|
||||
|
||||
|
||||
class CephPoolRemoveTierFailure(CephManagerException):
|
||||
message = _("Failed to remove tier: "
|
||||
"backing_pool=%(backing_pool)s, cache_pool=%(cache_pool)s, "
|
||||
"response=%(response_status_code)s:%(response_reason)s, "
|
||||
"status=%(status)s, output=%(output)s")
|
||||
|
||||
|
||||
class CephCacheSetModeFailure(CephManagerException):
|
||||
message = _("Failed to set OSD tier cache mode: "
|
||||
"cache_pool=%(cache_pool)s, mode=%(mode)s, "
|
||||
"response=%(response_status_code)s:%(response_reason)s, "
|
||||
"status=%(status)s, output=%(output)s")
|
||||
|
||||
|
||||
class CephPoolSetParamFailure(CephManagerException):
|
||||
message = _("Cannot set Ceph OSD pool parameter: "
|
||||
"pool_name=%(pool_name)s, param=%(param)s, value=%(value)s. "
|
||||
"Reason: %(reason)s")
|
||||
|
||||
|
||||
class CephPoolGetParamFailure(CephManagerException):
|
||||
message = _("Cannot get Ceph OSD pool parameter: "
|
||||
"pool_name=%(pool_name)s, param=%(param)s. "
|
||||
"Reason: %(reason)s")
|
||||
|
||||
|
||||
class CephCacheCreateOverlayFailure(CephManagerException):
|
||||
message = _("Failed to create overlay: "
|
||||
"backing_pool=%(backing_pool)s, cache_pool=%(cache_pool)s, "
|
||||
"response=%(response_status_code)s:%(response_reason)s, "
|
||||
"status=%(status)s, output=%(output)s")
|
||||
|
||||
|
||||
class CephCacheDeleteOverlayFailure(CephManagerException):
|
||||
message = _("Failed to delete overlay: "
|
||||
"backing_pool=%(backing_pool)s, cache_pool=%(cache_pool)s, "
|
||||
"response=%(response_status_code)s:%(response_reason)s, "
|
||||
"status=%(status)s, output=%(output)s")
|
||||
|
||||
|
||||
class CephCacheFlushFailure(CephManagerException):
|
||||
message = _("Failed to flush cache pool: "
|
||||
"cache_pool=%(cache_pool)s, "
|
||||
"return_code=%(return_code)s, "
|
||||
"cmd=%(cmd)s, output=%(output)s")
|
||||
|
||||
|
||||
class CephCacheEnableFailure(CephManagerException):
|
||||
message = _("Cannot enable Ceph cache tier. "
|
||||
"Reason: cache tiering operation in progress.")
|
||||
|
||||
|
||||
class CephCacheDisableFailure(CephManagerException):
|
||||
message = _("Cannot disable Ceph cache tier. "
|
||||
"Reason: cache tiering operation in progress.")
|
||||
|
||||
|
||||
class CephSetKeyFailure(CephManagerException):
|
||||
message = _("Error setting the Ceph flag "
|
||||
"'%(flag)s' %(extra)s: "
|
||||
"response=%(response_status_code)s:%(response_reason)s, "
|
||||
"status=%(status)s, output=%(output)s")
|
||||
|
||||
|
||||
class CephApiFailure(CephManagerException):
|
||||
message = _("API failure: "
|
||||
"call=%(call)s, reason=%(reason)s")
|
15
ceph/ceph-manager/ceph-manager/ceph_manager/i18n.py
Normal file
15
ceph/ceph-manager/ceph-manager/ceph_manager/i18n.py
Normal file
@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2016 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
import oslo_i18n
|
||||
|
||||
DOMAIN = 'ceph-manager'
|
||||
|
||||
_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN)
|
||||
_ = _translators.primary
|
||||
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
893
ceph/ceph-manager/ceph-manager/ceph_manager/monitor.py
Normal file
893
ceph/ceph-manager/ceph-manager/ceph_manager/monitor.py
Normal file
@ -0,0 +1,893 @@
|
||||
#
|
||||
# Copyright (c) 2013-2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import time
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from fm_api import fm_api
|
||||
# noinspection PyUnresolvedReferences
|
||||
from fm_api import constants as fm_constants
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_log import log as logging
|
||||
|
||||
from sysinv.conductor.cache_tiering_service_config import ServiceConfig
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
from i18n import _, _LI, _LW, _LE
|
||||
|
||||
import constants
|
||||
import exception
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# When upgrading from 16.10 to 17.x Ceph goes from Hammer release
|
||||
# to Jewel release. After all storage nodes are upgraded to 17.x
|
||||
# the cluster is in HEALTH_WARN until administrator explicitly
|
||||
# enables require_jewel_osds flag - which signals Ceph that it
|
||||
# can safely transition from Hammer to Jewel
|
||||
#
|
||||
# This class is needed only when upgrading from 16.10 to 17.x
|
||||
# TODO: remove it after 1st 17.x release
|
||||
#
|
||||
class HandleUpgradesMixin(object):
|
||||
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
self.surpress_require_jewel_osds_warning = False
|
||||
|
||||
def setup(self, config):
|
||||
self._set_upgrade(self.service.retry_get_software_upgrade_status())
|
||||
|
||||
def _set_upgrade(self, upgrade):
|
||||
state = upgrade.get('state')
|
||||
from_version = upgrade.get('from_version')
|
||||
if (state
|
||||
and state != constants.UPGRADE_COMPLETED
|
||||
and from_version == constants.TITANIUM_SERVER_VERSION_16_10):
|
||||
LOG.info(_LI("Surpress require_jewel_osds health warning"))
|
||||
self.surpress_require_jewel_osds_warning = True
|
||||
|
||||
def set_flag_require_jewel_osds(self):
|
||||
try:
|
||||
response, body = self.service.ceph_api.osd_set_key(
|
||||
constants.CEPH_FLAG_REQUIRE_JEWEL_OSDS,
|
||||
body='json')
|
||||
LOG.info(_LI("Set require_jewel_osds flag"))
|
||||
except IOError as e:
|
||||
raise exception.CephApiFailure(
|
||||
call="osd_set_key",
|
||||
reason=e.message)
|
||||
else:
|
||||
if not response.ok:
|
||||
raise exception.CephSetKeyFailure(
|
||||
flag=constants.CEPH_FLAG_REQUIRE_JEWEL_OSDS,
|
||||
extra=_("needed to complete upgrade to Jewel"),
|
||||
response_status_code=response.status_code,
|
||||
response_reason=response.reason,
|
||||
status=body.get('status'),
|
||||
output=body.get('output'))
|
||||
|
||||
def filter_health_status(self, health):
|
||||
health = self.auto_heal(health)
|
||||
# filter out require_jewel_osds warning
|
||||
#
|
||||
if not self.surpress_require_jewel_osds_warning:
|
||||
return health
|
||||
if health['health'] != constants.CEPH_HEALTH_WARN:
|
||||
return health
|
||||
if (constants.CEPH_HEALTH_WARN_REQUIRE_JEWEL_OSDS_NOT_SET
|
||||
not in health['detail']):
|
||||
return health
|
||||
return self._remove_require_jewel_osds_warning(health)
|
||||
|
||||
def _remove_require_jewel_osds_warning(self, health):
|
||||
reasons_list = []
|
||||
for reason in health['detail'].split(';'):
|
||||
reason = reason.strip()
|
||||
if len(reason) == 0:
|
||||
continue
|
||||
if constants.CEPH_HEALTH_WARN_REQUIRE_JEWEL_OSDS_NOT_SET in reason:
|
||||
continue
|
||||
reasons_list.append(reason)
|
||||
if len(reasons_list) == 0:
|
||||
health = {
|
||||
'health': constants.CEPH_HEALTH_OK,
|
||||
'detail': ''}
|
||||
else:
|
||||
health['detail'] = '; '.join(reasons_list)
|
||||
return health
|
||||
|
||||
def auto_heal(self, health):
|
||||
if (health['health'] == constants.CEPH_HEALTH_WARN
|
||||
and (constants.CEPH_HEALTH_WARN_REQUIRE_JEWEL_OSDS_NOT_SET
|
||||
in health['detail'])):
|
||||
try:
|
||||
upgrade = self.service.get_software_upgrade_status()
|
||||
except Exception as ex:
|
||||
LOG.warn(_LW(
|
||||
"Getting software upgrade status failed "
|
||||
"with: %s. Skip auto-heal attempt "
|
||||
"(will retry on next ceph status poll).") % str(ex))
|
||||
return
|
||||
state = upgrade.get('state')
|
||||
# surpress require_jewel_osds in case upgrade is
|
||||
# in progress but not completed or aborting
|
||||
if (not self.surpress_require_jewel_osds_warning
|
||||
and (upgrade.get('from_version')
|
||||
== constants.TITANIUM_SERVER_VERSION_16_10)
|
||||
and state not in [
|
||||
None,
|
||||
constants.UPGRADE_COMPLETED,
|
||||
constants.UPGRADE_ABORTING,
|
||||
constants.UPGRADE_ABORT_COMPLETING,
|
||||
constants.UPGRADE_ABORTING_ROLLBACK]):
|
||||
LOG.info(_LI("Surpress require_jewel_osds health warning"))
|
||||
self.surpress_require_jewel_osds_warning = True
|
||||
# set require_jewel_osds in case upgrade is
|
||||
# not in progress or completed
|
||||
if (state in [None, constants.UPGRADE_COMPLETED]):
|
||||
LOG.warn(_LW(
|
||||
"No upgrade in progress or update completed "
|
||||
"and require_jewel_osds health warning raised. "
|
||||
"Set require_jewel_osds flag."))
|
||||
self.set_flag_require_jewel_osds()
|
||||
health = self._remove_require_jewel_osds_warning(health)
|
||||
LOG.info(_LI("Unsurpress require_jewel_osds health warning"))
|
||||
self.surpress_require_jewel_osds_warning = False
|
||||
# unsurpress require_jewel_osds in case upgrade
|
||||
# is aborting
|
||||
if (self.surpress_require_jewel_osds_warning
|
||||
and state in [
|
||||
constants.UPGRADE_ABORTING,
|
||||
constants.UPGRADE_ABORT_COMPLETING,
|
||||
constants.UPGRADE_ABORTING_ROLLBACK]):
|
||||
LOG.info(_LI("Unsurpress require_jewel_osds health warning"))
|
||||
self.surpress_require_jewel_osds_warning = False
|
||||
return health
|
||||
|
||||
|
||||
class Monitor(HandleUpgradesMixin):
|
||||
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
self.current_ceph_health = ""
|
||||
self.cache_enabled = False
|
||||
self.tiers_size = {}
|
||||
self.known_object_pool_name = None
|
||||
self.primary_tier_name = constants.SB_TIER_DEFAULT_NAMES[
|
||||
constants.SB_TIER_TYPE_CEPH] + constants.CEPH_CRUSH_TIER_SUFFIX
|
||||
self.cluster_is_up = False
|
||||
super(Monitor, self).__init__(service)
|
||||
|
||||
def setup(self, config):
|
||||
self.set_caching_tier_config(config)
|
||||
super(Monitor, self).setup(config)
|
||||
|
||||
def set_caching_tier_config(self, config):
|
||||
conf = ServiceConfig().from_dict(
|
||||
config.get(constants.CACHE_TIERING_APPLIED))
|
||||
if conf:
|
||||
self.cache_enabled = conf.cache_enabled
|
||||
|
||||
def monitor_check_cache_tier(self, enable_flag):
|
||||
LOG.info(_LI("monitor_check_cache_tier: "
|
||||
"enable_flag={}".format(enable_flag)))
|
||||
self.cache_enabled = enable_flag
|
||||
|
||||
def run(self):
|
||||
# Wait until Ceph cluster is up and we can get the fsid
|
||||
while True:
|
||||
self.ceph_get_fsid()
|
||||
if self.service.entity_instance_id:
|
||||
break
|
||||
time.sleep(constants.CEPH_HEALTH_CHECK_INTERVAL)
|
||||
|
||||
# Start monitoring ceph status
|
||||
while True:
|
||||
self.ceph_poll_status()
|
||||
self.ceph_poll_quotas()
|
||||
time.sleep(constants.CEPH_HEALTH_CHECK_INTERVAL)
|
||||
|
||||
def ceph_get_fsid(self):
|
||||
# Check whether an alarm has already been raised
|
||||
self._get_current_alarms()
|
||||
if self.current_health_alarm:
|
||||
LOG.info(_LI("Current alarm: %s") %
|
||||
str(self.current_health_alarm.__dict__))
|
||||
|
||||
fsid = self._get_fsid()
|
||||
if not fsid:
|
||||
# Raise alarm - it will not have an entity_instance_id
|
||||
self._report_fault({'health': constants.CEPH_HEALTH_DOWN,
|
||||
'detail': 'Ceph cluster is down.'},
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH)
|
||||
else:
|
||||
# Clear alarm with no entity_instance_id
|
||||
self._clear_fault(fm_constants.FM_ALARM_ID_STORAGE_CEPH)
|
||||
self.service.entity_instance_id = 'cluster=%s' % fsid
|
||||
|
||||
def ceph_poll_status(self):
|
||||
# get previous data every time in case:
|
||||
# * daemon restarted
|
||||
# * alarm was cleared manually but stored as raised in daemon
|
||||
self._get_current_alarms()
|
||||
if self.current_health_alarm:
|
||||
LOG.info(_LI("Current alarm: %s") %
|
||||
str(self.current_health_alarm.__dict__))
|
||||
|
||||
# get ceph health
|
||||
health = self._get_health()
|
||||
LOG.info(_LI("Current Ceph health: "
|
||||
"%(health)s detail: %(detail)s") % health)
|
||||
|
||||
health = self.filter_health_status(health)
|
||||
if health['health'] != constants.CEPH_HEALTH_OK:
|
||||
self._report_fault(health, fm_constants.FM_ALARM_ID_STORAGE_CEPH)
|
||||
self._report_alarm_osds_health()
|
||||
else:
|
||||
self._clear_fault(fm_constants.FM_ALARM_ID_STORAGE_CEPH)
|
||||
self.clear_all_major_critical()
|
||||
|
||||
def filter_health_status(self, health):
|
||||
return super(Monitor, self).filter_health_status(health)
|
||||
|
||||
def ceph_poll_quotas(self):
|
||||
self._get_current_alarms()
|
||||
if self.current_quota_alarms:
|
||||
LOG.info(_LI("Current quota alarms %s") %
|
||||
self.current_quota_alarms)
|
||||
|
||||
# Get current current size of each tier
|
||||
previous_tiers_size = self.tiers_size
|
||||
self.tiers_size = self._get_tiers_size()
|
||||
|
||||
# Make sure any removed tiers have the alarms cleared
|
||||
for t in (set(previous_tiers_size)-set(self.tiers_size)):
|
||||
self._clear_fault(fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE,
|
||||
"{0}.tier={1}".format(
|
||||
self.service.entity_instance_id,
|
||||
t[:-len(constants.CEPH_CRUSH_TIER_SUFFIX)]))
|
||||
|
||||
# Check the quotas on each tier
|
||||
for tier in self.tiers_size:
|
||||
# TODO(rchurch): For R6 remove the tier from the default crushmap
|
||||
# and remove this check. No longer supporting this tier in R5
|
||||
if tier == 'cache-tier':
|
||||
continue
|
||||
|
||||
# Extract the tier name from the crush equivalent
|
||||
tier_name = tier[:-len(constants.CEPH_CRUSH_TIER_SUFFIX)]
|
||||
|
||||
if self.tiers_size[tier] == 0:
|
||||
LOG.info(_LI("'%s' tier cluster size not yet available")
|
||||
% tier_name)
|
||||
continue
|
||||
|
||||
pools_quota_sum = 0
|
||||
if tier == self.primary_tier_name:
|
||||
for pool in constants.CEPH_POOLS:
|
||||
if (pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL or
|
||||
pool['pool_name'] ==
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER):
|
||||
object_pool_name = self._get_object_pool_name()
|
||||
if object_pool_name is None:
|
||||
LOG.error("Rados gateway object data pool does "
|
||||
"not exist.")
|
||||
else:
|
||||
pools_quota_sum += \
|
||||
self._get_osd_pool_quota(object_pool_name)
|
||||
else:
|
||||
pools_quota_sum += self._get_osd_pool_quota(
|
||||
pool['pool_name'])
|
||||
else:
|
||||
for pool in constants.SB_TIER_CEPH_POOLS:
|
||||
pool_name = "{0}-{1}".format(pool['pool_name'], tier_name)
|
||||
pools_quota_sum += self._get_osd_pool_quota(pool_name)
|
||||
|
||||
# Currently, there is only one pool on the addtional tier(s),
|
||||
# therefore allow a quota of 0
|
||||
if (pools_quota_sum != self.tiers_size[tier] and
|
||||
pools_quota_sum != 0):
|
||||
self._report_fault(
|
||||
{'tier_name': tier_name,
|
||||
'tier_eid': "{0}.tier={1}".format(
|
||||
self.service.entity_instance_id,
|
||||
tier_name)},
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE)
|
||||
else:
|
||||
self._clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE,
|
||||
"{0}.tier={1}".format(self.service.entity_instance_id,
|
||||
tier_name))
|
||||
|
||||
# CEPH HELPERS
|
||||
|
||||
def _get_fsid(self):
|
||||
try:
|
||||
response, fsid = self.service.ceph_api.fsid(
|
||||
body='text', timeout=30)
|
||||
except IOError as e:
|
||||
LOG.warning(_LW("ceph_api.fsid failed: %s") % str(e.message))
|
||||
self.cluster_is_up = False
|
||||
return None
|
||||
|
||||
if not response.ok:
|
||||
LOG.warning(_LW("Get fsid failed: %s") % response.reason)
|
||||
self.cluster_is_up = False
|
||||
return None
|
||||
|
||||
self.cluster_is_up = True
|
||||
return fsid.strip()
|
||||
|
||||
def _get_health(self):
|
||||
try:
|
||||
# we use text since it has all info
|
||||
response, body = self.service.ceph_api.health(
|
||||
body='text', timeout=30)
|
||||
except IOError as e:
|
||||
LOG.warning(_LW("ceph_api.health failed: %s") % str(e.message))
|
||||
self.cluster_is_up = False
|
||||
return {'health': constants.CEPH_HEALTH_DOWN,
|
||||
'detail': 'Ceph cluster is down.'}
|
||||
|
||||
if not response.ok:
|
||||
LOG.warning(_LW("CEPH health check failed: %s") % response.reason)
|
||||
health_info = [constants.CEPH_HEALTH_DOWN, response.reason]
|
||||
self.cluster_is_up = False
|
||||
else:
|
||||
health_info = body.split(' ', 1)
|
||||
self.cluster_is_up = True
|
||||
|
||||
health = health_info[0]
|
||||
|
||||
if len(health_info) > 1:
|
||||
detail = health_info[1]
|
||||
else:
|
||||
detail = health_info[0]
|
||||
|
||||
return {'health': health.strip(),
|
||||
'detail': detail.strip()}
|
||||
|
||||
def _get_object_pool_name(self):
|
||||
if self.known_object_pool_name is None:
|
||||
response, body = self.service.ceph_api.osd_pool_get(
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL,
|
||||
"pg_num",
|
||||
body='json')
|
||||
|
||||
if response.ok:
|
||||
self.known_object_pool_name = \
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_JEWEL
|
||||
return self.known_object_pool_name
|
||||
|
||||
response, body = self.service.ceph_api.osd_pool_get(
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER,
|
||||
"pg_num",
|
||||
body='json')
|
||||
|
||||
if response.ok:
|
||||
self.known_object_pool_name = \
|
||||
constants.CEPH_POOL_OBJECT_GATEWAY_NAME_HAMMER
|
||||
return self.known_object_pool_name
|
||||
|
||||
return self.known_object_pool_name
|
||||
|
||||
def _get_osd_pool_quota(self, pool_name):
|
||||
try:
|
||||
resp, quota = self.service.ceph_api.osd_get_pool_quota(
|
||||
pool_name, body='json')
|
||||
except IOError:
|
||||
return 0
|
||||
|
||||
if not resp.ok:
|
||||
LOG.error(_LE("Getting the quota for "
|
||||
"%(name)s pool failed:%(reason)s)") %
|
||||
{"name": pool_name, "reason": resp.reason})
|
||||
return 0
|
||||
else:
|
||||
try:
|
||||
quota_gib = int(quota["output"]["quota_max_bytes"])/(1024**3)
|
||||
return quota_gib
|
||||
except IOError:
|
||||
return 0
|
||||
|
||||
# we have two root nodes 'cache-tier' and 'storage-tier'
|
||||
# to calculate the space that is used by the pools, we must only
|
||||
# use 'storage-tier'
|
||||
# this function determines if a certain node is under a certain
|
||||
# tree
|
||||
def host_is_in_root(self, search_tree, node, root_name):
|
||||
if node['type'] == 'root':
|
||||
if node['name'] == root_name:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return self.host_is_in_root(search_tree,
|
||||
search_tree[node['parent']],
|
||||
root_name)
|
||||
|
||||
# The information received from ceph is not properly
|
||||
# structured for efficient parsing and searching, so
|
||||
# it must be processed and transformed into a more
|
||||
# structured form.
|
||||
#
|
||||
# Input received from ceph is an array of nodes with the
|
||||
# following structure:
|
||||
# [{'id':<node_id>, 'children':<array_of_children_ids>, ....},
|
||||
# ...]
|
||||
#
|
||||
# We process this array and transform it into a dictionary
|
||||
# (for efficient access) The transformed "search tree" is a
|
||||
# dictionary with the following structure:
|
||||
# {<node_id> : {'children':<array_of_children_ids>}
|
||||
def _get_tiers_size(self):
|
||||
try:
|
||||
resp, body = self.service.ceph_api.osd_df(
|
||||
body='json',
|
||||
output_method='tree')
|
||||
except IOError:
|
||||
return 0
|
||||
if not resp.ok:
|
||||
LOG.error(_LE("Getting the cluster usage "
|
||||
"information failed: %(reason)s - "
|
||||
"%(body)s") % {"reason": resp.reason,
|
||||
"body": body})
|
||||
return {}
|
||||
|
||||
# A node is a crushmap element: root, chassis, host, osd. Create a
|
||||
# dictionary for the nodes with the key as the id used for efficient
|
||||
# searching through nodes.
|
||||
#
|
||||
# For example: storage-0's node has one child node => OSD 0
|
||||
# {
|
||||
# "id": -4,
|
||||
# "name": "storage-0",
|
||||
# "type": "host",
|
||||
# "type_id": 1,
|
||||
# "reweight": -1.000000,
|
||||
# "kb": 51354096,
|
||||
# "kb_used": 1510348,
|
||||
# "kb_avail": 49843748,
|
||||
# "utilization": 2.941047,
|
||||
# "var": 1.480470,
|
||||
# "pgs": 0,
|
||||
# "children": [
|
||||
# 0
|
||||
# ]
|
||||
# },
|
||||
search_tree = {}
|
||||
for node in body['output']['nodes']:
|
||||
search_tree[node['id']] = node
|
||||
|
||||
# Extract the tiers as we will return a dict for the size of each tier
|
||||
tiers = {k: v for k, v in search_tree.items() if v['type'] == 'root'}
|
||||
|
||||
# For each tier, traverse the heirarchy from the root->chassis->host.
|
||||
# Sum the host sizes to determine the overall size of the tier
|
||||
tier_sizes = {}
|
||||
for tier in tiers.values():
|
||||
tier_size = 0
|
||||
for chassis_id in tier['children']:
|
||||
chassis_size = 0
|
||||
chassis = search_tree[chassis_id]
|
||||
for host_id in chassis['children']:
|
||||
host = search_tree[host_id]
|
||||
if (chassis_size == 0 or
|
||||
chassis_size > host['kb']):
|
||||
chassis_size = host['kb']
|
||||
tier_size += chassis_size/(1024 ** 2)
|
||||
tier_sizes[tier['name']] = tier_size
|
||||
|
||||
return tier_sizes
|
||||
|
||||
# ALARM HELPERS
|
||||
|
||||
@staticmethod
|
||||
def _check_storage_group(osd_tree, group_id,
|
||||
hosts, osds, fn_report_alarm):
|
||||
reasons = set()
|
||||
degraded_hosts = set()
|
||||
severity = fm_constants.FM_ALARM_SEVERITY_CRITICAL
|
||||
for host_id in hosts:
|
||||
if len(osds[host_id]) == 0:
|
||||
reasons.add(constants.ALARM_REASON_NO_OSD)
|
||||
degraded_hosts.add(host_id)
|
||||
else:
|
||||
for osd_id in osds[host_id]:
|
||||
if osd_tree[osd_id]['status'] == 'up':
|
||||
if osd_tree[osd_id]['reweight'] == 0.0:
|
||||
reasons.add(constants.ALARM_REASON_OSDS_OUT)
|
||||
degraded_hosts.add(host_id)
|
||||
else:
|
||||
severity = fm_constants.FM_ALARM_SEVERITY_MAJOR
|
||||
elif osd_tree[osd_id]['status'] == 'down':
|
||||
reasons.add(constants.ALARM_REASON_OSDS_DOWN)
|
||||
degraded_hosts.add(host_id)
|
||||
if constants.ALARM_REASON_OSDS_OUT in reasons \
|
||||
and constants.ALARM_REASON_OSDS_DOWN in reasons:
|
||||
reasons.add(constants.ALARM_REASON_OSDS_DOWN_OUT)
|
||||
reasons.remove(constants.ALARM_REASON_OSDS_OUT)
|
||||
if constants.ALARM_REASON_OSDS_DOWN in reasons \
|
||||
and constants.ALARM_REASON_OSDS_DOWN_OUT in reasons:
|
||||
reasons.remove(constants.ALARM_REASON_OSDS_DOWN)
|
||||
reason = "/".join(list(reasons))
|
||||
if severity == fm_constants.FM_ALARM_SEVERITY_CRITICAL:
|
||||
reason = "{} {}: {}".format(
|
||||
fm_constants.ALARM_CRITICAL_REPLICATION,
|
||||
osd_tree[group_id]['name'],
|
||||
reason)
|
||||
elif severity == fm_constants.FM_ALARM_SEVERITY_MAJOR:
|
||||
reason = "{} {}: {}".format(
|
||||
fm_constants.ALARM_MAJOR_REPLICATION,
|
||||
osd_tree[group_id]['name'],
|
||||
reason)
|
||||
if len(degraded_hosts) == 0:
|
||||
if len(hosts) < 2:
|
||||
fn_report_alarm(
|
||||
osd_tree[group_id]['name'],
|
||||
"{} {}: {}".format(
|
||||
fm_constants.ALARM_MAJOR_REPLICATION,
|
||||
osd_tree[group_id]['name'],
|
||||
constants.ALARM_REASON_PEER_HOST_DOWN),
|
||||
fm_constants.FM_ALARM_SEVERITY_MAJOR)
|
||||
elif len(degraded_hosts) == 1:
|
||||
fn_report_alarm(
|
||||
"{}.host={}".format(
|
||||
osd_tree[group_id]['name'],
|
||||
osd_tree[list(degraded_hosts)[0]]['name']),
|
||||
reason, severity)
|
||||
else:
|
||||
fn_report_alarm(
|
||||
osd_tree[group_id]['name'],
|
||||
reason, severity)
|
||||
|
||||
def _check_storage_tier(self, osd_tree, tier_name, fn_report_alarm):
|
||||
for tier_id in osd_tree:
|
||||
if osd_tree[tier_id]['type'] != 'root':
|
||||
continue
|
||||
if osd_tree[tier_id]['name'] != tier_name:
|
||||
continue
|
||||
for group_id in osd_tree[tier_id]['children']:
|
||||
if osd_tree[group_id]['type'] != 'chassis':
|
||||
continue
|
||||
if not osd_tree[group_id]['name'].startswith('group-'):
|
||||
continue
|
||||
hosts = []
|
||||
osds = {}
|
||||
for host_id in osd_tree[group_id]['children']:
|
||||
if osd_tree[host_id]['type'] != 'host':
|
||||
continue
|
||||
hosts.append(host_id)
|
||||
osds[host_id] = []
|
||||
for osd_id in osd_tree[host_id]['children']:
|
||||
if osd_tree[osd_id]['type'] == 'osd':
|
||||
osds[host_id].append(osd_id)
|
||||
self._check_storage_group(osd_tree, group_id, hosts,
|
||||
osds, fn_report_alarm)
|
||||
break
|
||||
|
||||
def _current_health_alarm_equals(self, reason, severity):
|
||||
if not self.current_health_alarm:
|
||||
return False
|
||||
if getattr(self.current_health_alarm, 'severity', None) != severity:
|
||||
return False
|
||||
if getattr(self.current_health_alarm, 'reason_text', None) != reason:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _report_alarm_osds_health(self):
|
||||
response, osd_tree = self.service.ceph_api.osd_tree(body='json')
|
||||
if not response.ok:
|
||||
LOG.error(_LE("Failed to retrieve Ceph OSD tree: "
|
||||
"status_code: %(status_code)s, reason: %(reason)s") %
|
||||
{"status_code": response.status_code,
|
||||
"reason": response.reason})
|
||||
return
|
||||
osd_tree = dict([(n['id'], n) for n in osd_tree['output']['nodes']])
|
||||
alarms = []
|
||||
|
||||
self._check_storage_tier(osd_tree, "storage-tier",
|
||||
lambda *args: alarms.append(args))
|
||||
if self.cache_enabled:
|
||||
self._check_storage_tier(osd_tree, "cache-tier",
|
||||
lambda *args: alarms.append(args))
|
||||
|
||||
old_alarms = {}
|
||||
for alarm_id in [
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_MAJOR,
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_CRITICAL]:
|
||||
alarm_list = self.service.fm_api.get_faults_by_id(alarm_id)
|
||||
if not alarm_list:
|
||||
continue
|
||||
for alarm in alarm_list:
|
||||
if alarm.entity_instance_id not in old_alarms:
|
||||
old_alarms[alarm.entity_instance_id] = []
|
||||
old_alarms[alarm.entity_instance_id].append(
|
||||
(alarm.alarm_id, alarm.reason_text))
|
||||
|
||||
for peer_group, reason, severity in alarms:
|
||||
if self._current_health_alarm_equals(reason, severity):
|
||||
continue
|
||||
alarm_critical_major = fm_constants.FM_ALARM_ID_STORAGE_CEPH_MAJOR
|
||||
if severity == fm_constants.FM_ALARM_SEVERITY_CRITICAL:
|
||||
alarm_critical_major = (
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_CRITICAL)
|
||||
entity_instance_id = (
|
||||
self.service.entity_instance_id + '.peergroup=' + peer_group)
|
||||
alarm_already_exists = False
|
||||
if entity_instance_id in old_alarms:
|
||||
for alarm_id, old_reason in old_alarms[entity_instance_id]:
|
||||
if (reason == old_reason and
|
||||
alarm_id == alarm_critical_major):
|
||||
# if the alarm is exactly the same, we don't need
|
||||
# to recreate it
|
||||
old_alarms[entity_instance_id].remove(
|
||||
(alarm_id, old_reason))
|
||||
alarm_already_exists = True
|
||||
elif (alarm_id == alarm_critical_major):
|
||||
# if we change just the reason, then we just remove the
|
||||
# alarm from the list so we don't remove it at the
|
||||
# end of the function
|
||||
old_alarms[entity_instance_id].remove(
|
||||
(alarm_id, old_reason))
|
||||
|
||||
if (len(old_alarms[entity_instance_id]) == 0):
|
||||
del old_alarms[entity_instance_id]
|
||||
|
||||
# in case the alarm is exactly the same, we skip the alarm set
|
||||
if alarm_already_exists is True:
|
||||
continue
|
||||
major_repair_action = constants.REPAIR_ACTION_MAJOR_CRITICAL_ALARM
|
||||
fault = fm_api.Fault(
|
||||
alarm_id=alarm_critical_major,
|
||||
alarm_type=fm_constants.FM_ALARM_TYPE_4,
|
||||
alarm_state=fm_constants.FM_ALARM_STATE_SET,
|
||||
entity_type_id=fm_constants.FM_ENTITY_TYPE_CLUSTER,
|
||||
entity_instance_id=entity_instance_id,
|
||||
severity=severity,
|
||||
reason_text=reason,
|
||||
probable_cause=fm_constants.ALARM_PROBABLE_CAUSE_15,
|
||||
proposed_repair_action=major_repair_action,
|
||||
service_affecting=constants.SERVICE_AFFECTING['HEALTH_WARN'])
|
||||
alarm_uuid = self.service.fm_api.set_fault(fault)
|
||||
if alarm_uuid:
|
||||
LOG.info(_LI(
|
||||
"Created storage alarm %(alarm_uuid)s - "
|
||||
"severity: %(severity)s, reason: %(reason)s, "
|
||||
"service_affecting: %(service_affecting)s") % {
|
||||
"alarm_uuid": str(alarm_uuid),
|
||||
"severity": str(severity),
|
||||
"reason": reason,
|
||||
"service_affecting": str(
|
||||
constants.SERVICE_AFFECTING['HEALTH_WARN'])})
|
||||
else:
|
||||
LOG.error(_LE(
|
||||
"Failed to create storage alarm - "
|
||||
"severity: %(severity)s, reason: %(reason)s, "
|
||||
"service_affecting: %(service_affecting)s") % {
|
||||
"severity": str(severity),
|
||||
"reason": reason,
|
||||
"service_affecting": str(
|
||||
constants.SERVICE_AFFECTING['HEALTH_WARN'])})
|
||||
|
||||
for entity_instance_id in old_alarms:
|
||||
for alarm_id, old_reason in old_alarms[entity_instance_id]:
|
||||
self.service.fm_api.clear_fault(alarm_id, entity_instance_id)
|
||||
|
||||
@staticmethod
|
||||
def _parse_reason(health):
|
||||
""" Parse reason strings received from Ceph """
|
||||
if health['health'] in constants.CEPH_STATUS_CUSTOM:
|
||||
# Don't parse reason messages that we added
|
||||
return "Storage Alarm Condition: %(health)s. %(detail)s" % health
|
||||
|
||||
reasons_lst = health['detail'].split(';')
|
||||
|
||||
parsed_reasons_text = ""
|
||||
|
||||
# Check if PGs have issues - we can't safely store the entire message
|
||||
# as it tends to be long
|
||||
for reason in reasons_lst:
|
||||
if "pgs" in reason:
|
||||
parsed_reasons_text += "PGs are degraded/stuck or undersized"
|
||||
break
|
||||
|
||||
# Extract recovery status
|
||||
parsed_reasons = [r.strip() for r in reasons_lst if 'recovery' in r]
|
||||
if parsed_reasons:
|
||||
parsed_reasons_text += ";" + ";".join(parsed_reasons)
|
||||
|
||||
# We need to keep the most important parts of the messages when storing
|
||||
# them to fm alarms, therefore text between [] brackets is truncated if
|
||||
# max size is reached.
|
||||
|
||||
# Add brackets, if needed
|
||||
if len(parsed_reasons_text):
|
||||
lbracket = " ["
|
||||
rbracket = "]"
|
||||
else:
|
||||
lbracket = ""
|
||||
rbracket = ""
|
||||
|
||||
msg = {"head": "Storage Alarm Condition: ",
|
||||
"tail": ". Please check 'ceph -s' for more details."}
|
||||
max_size = constants.FM_ALARM_REASON_MAX_SIZE - \
|
||||
len(msg["head"]) - len(msg["tail"])
|
||||
|
||||
return (
|
||||
msg['head'] +
|
||||
(health['health'] + lbracket + parsed_reasons_text)[:max_size-1] +
|
||||
rbracket + msg['tail'])
|
||||
|
||||
def _report_fault(self, health, alarm_id):
|
||||
if alarm_id == fm_constants.FM_ALARM_ID_STORAGE_CEPH:
|
||||
new_severity = constants.SEVERITY[health['health']]
|
||||
new_reason_text = self._parse_reason(health)
|
||||
new_service_affecting = \
|
||||
constants.SERVICE_AFFECTING[health['health']]
|
||||
|
||||
# Raise or update alarm if necessary
|
||||
if ((not self.current_health_alarm) or
|
||||
(self.current_health_alarm.__dict__['severity'] !=
|
||||
new_severity) or
|
||||
(self.current_health_alarm.__dict__['reason_text'] !=
|
||||
new_reason_text) or
|
||||
(self.current_health_alarm.__dict__['service_affecting'] !=
|
||||
str(new_service_affecting))):
|
||||
|
||||
fault = fm_api.Fault(
|
||||
alarm_id=fm_constants.FM_ALARM_ID_STORAGE_CEPH,
|
||||
alarm_type=fm_constants.FM_ALARM_TYPE_4,
|
||||
alarm_state=fm_constants.FM_ALARM_STATE_SET,
|
||||
entity_type_id=fm_constants.FM_ENTITY_TYPE_CLUSTER,
|
||||
entity_instance_id=self.service.entity_instance_id,
|
||||
severity=new_severity,
|
||||
reason_text=new_reason_text,
|
||||
probable_cause=fm_constants.ALARM_PROBABLE_CAUSE_15,
|
||||
proposed_repair_action=constants.REPAIR_ACTION,
|
||||
service_affecting=new_service_affecting)
|
||||
|
||||
alarm_uuid = self.service.fm_api.set_fault(fault)
|
||||
if alarm_uuid:
|
||||
LOG.info(_LI(
|
||||
"Created storage alarm %(alarm_uuid)s - "
|
||||
"severity: %(severity)s, reason: %(reason)s, "
|
||||
"service_affecting: %(service_affecting)s") % {
|
||||
"alarm_uuid": alarm_uuid,
|
||||
"severity": new_severity,
|
||||
"reason": new_reason_text,
|
||||
"service_affecting": new_service_affecting})
|
||||
else:
|
||||
LOG.error(_LE(
|
||||
"Failed to create storage alarm - "
|
||||
"severity: %(severity)s, reason: %(reason)s "
|
||||
"service_affecting: %(service_affecting)s") % {
|
||||
"severity": new_severity,
|
||||
"reason": new_reason_text,
|
||||
"service_affecting": new_service_affecting})
|
||||
|
||||
# Log detailed reason for later analysis
|
||||
if (self.current_ceph_health != health['health'] or
|
||||
self.detailed_health_reason != health['detail']):
|
||||
LOG.info(_LI("Ceph status changed: %(health)s "
|
||||
"detailed reason: %(detail)s") % health)
|
||||
self.current_ceph_health = health['health']
|
||||
self.detailed_health_reason = health['detail']
|
||||
|
||||
elif (alarm_id == fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE and
|
||||
not health['tier_eid'] in self.current_quota_alarms):
|
||||
|
||||
quota_reason_text = ("Quota/Space mismatch for the %s tier. The "
|
||||
"sum of Ceph pool quotas does not match the "
|
||||
"tier size." % health['tier_name'])
|
||||
fault = fm_api.Fault(
|
||||
alarm_id=fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE,
|
||||
alarm_state=fm_constants.FM_ALARM_STATE_SET,
|
||||
entity_type_id=fm_constants.FM_ENTITY_TYPE_CLUSTER,
|
||||
entity_instance_id=health['tier_eid'],
|
||||
severity=fm_constants.FM_ALARM_SEVERITY_MINOR,
|
||||
reason_text=quota_reason_text,
|
||||
alarm_type=fm_constants.FM_ALARM_TYPE_7,
|
||||
probable_cause=fm_constants.ALARM_PROBABLE_CAUSE_75,
|
||||
proposed_repair_action=(
|
||||
"Update ceph storage pool quotas to use all available "
|
||||
"cluster space for the %s tier." % health['tier_name']),
|
||||
service_affecting=False)
|
||||
|
||||
alarm_uuid = self.service.fm_api.set_fault(fault)
|
||||
if alarm_uuid:
|
||||
LOG.info(_LI(
|
||||
"Created storage quota storage alarm %(alarm_uuid)s. "
|
||||
"Reason: %(reason)s") % {
|
||||
"alarm_uuid": alarm_uuid, "reason": quota_reason_text})
|
||||
else:
|
||||
LOG.error(_LE("Failed to create quota "
|
||||
"storage alarm. Reason: %s") % quota_reason_text)
|
||||
|
||||
def _clear_fault(self, alarm_id, entity_instance_id=None):
|
||||
# Only clear alarm if there is one already raised
|
||||
if (alarm_id == fm_constants.FM_ALARM_ID_STORAGE_CEPH and
|
||||
self.current_health_alarm):
|
||||
LOG.info(_LI("Clearing health alarm"))
|
||||
self.service.fm_api.clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH,
|
||||
self.service.entity_instance_id)
|
||||
elif (alarm_id == fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE and
|
||||
entity_instance_id in self.current_quota_alarms):
|
||||
LOG.info(_LI("Clearing quota alarm with entity_instance_id %s")
|
||||
% entity_instance_id)
|
||||
self.service.fm_api.clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE,
|
||||
entity_instance_id)
|
||||
|
||||
def clear_critical_alarm(self, group_name):
|
||||
alarm_list = self.service.fm_api.get_faults_by_id(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_CRITICAL)
|
||||
if alarm_list:
|
||||
for alarm in range(len(alarm_list)):
|
||||
group_id = alarm_list[alarm].entity_instance_id.find("group-")
|
||||
group_instance_name = (
|
||||
"group-" +
|
||||
alarm_list[alarm].entity_instance_id[group_id + 6])
|
||||
if group_name == group_instance_name:
|
||||
self.service.fm_api.clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_CRITICAL,
|
||||
alarm_list[alarm].entity_instance_id)
|
||||
|
||||
def clear_all_major_critical(self, group_name=None):
|
||||
# clear major alarms
|
||||
alarm_list = self.service.fm_api.get_faults_by_id(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_MAJOR)
|
||||
if alarm_list:
|
||||
for alarm in range(len(alarm_list)):
|
||||
if group_name is not None:
|
||||
group_id = (
|
||||
alarm_list[alarm].entity_instance_id.find("group-"))
|
||||
group_instance_name = (
|
||||
"group-" +
|
||||
alarm_list[alarm].entity_instance_id[group_id+6])
|
||||
if group_name == group_instance_name:
|
||||
self.service.fm_api.clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_MAJOR,
|
||||
alarm_list[alarm].entity_instance_id)
|
||||
else:
|
||||
self.service.fm_api.clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_MAJOR,
|
||||
alarm_list[alarm].entity_instance_id)
|
||||
# clear critical alarms
|
||||
alarm_list = self.service.fm_api.get_faults_by_id(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_CRITICAL)
|
||||
if alarm_list:
|
||||
for alarm in range(len(alarm_list)):
|
||||
if group_name is not None:
|
||||
group_id = (
|
||||
alarm_list[alarm].entity_instance_id.find("group-"))
|
||||
group_instance_name = (
|
||||
"group-" +
|
||||
alarm_list[alarm].entity_instance_id[group_id + 6])
|
||||
if group_name == group_instance_name:
|
||||
self.service.fm_api.clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_CRITICAL,
|
||||
alarm_list[alarm].entity_instance_id)
|
||||
else:
|
||||
self.service.fm_api.clear_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_CRITICAL,
|
||||
alarm_list[alarm].entity_instance_id)
|
||||
|
||||
def _get_current_alarms(self):
|
||||
""" Retrieve currently raised alarm """
|
||||
self.current_health_alarm = self.service.fm_api.get_fault(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH,
|
||||
self.service.entity_instance_id)
|
||||
quota_faults = self.service.fm_api.get_faults_by_id(
|
||||
fm_constants.FM_ALARM_ID_STORAGE_CEPH_FREE_SPACE)
|
||||
if quota_faults:
|
||||
self.current_quota_alarms = [f.entity_instance_id
|
||||
for f in quota_faults]
|
||||
else:
|
||||
self.current_quota_alarms = []
|
249
ceph/ceph-manager/ceph-manager/ceph_manager/server.py
Normal file
249
ceph/ceph-manager/ceph-manager/ceph_manager/server.py
Normal file
@ -0,0 +1,249 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright (c) 2016-2018 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# https://chrigl.de/posts/2014/08/27/oslo-messaging-example.html
|
||||
# http://docs.openstack.org/developer/oslo.messaging/server.html
|
||||
|
||||
import sys
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
import eventlet
|
||||
# noinspection PyUnresolvedReferences
|
||||
import oslo_messaging as messaging
|
||||
# noinspection PyUnresolvedReferences
|
||||
from fm_api import fm_api
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_config import cfg
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_log import log as logging
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_service import service
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_service.periodic_task import PeriodicTasks
|
||||
# noinspection PyUnresolvedReferences
|
||||
from oslo_service import loopingcall
|
||||
|
||||
from sysinv.conductor.cache_tiering_service_config import ServiceConfig
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from cephclient import wrapper
|
||||
|
||||
from monitor import Monitor
|
||||
from cache_tiering import CacheTiering
|
||||
import exception
|
||||
import constants
|
||||
|
||||
from i18n import _LI, _LW
|
||||
from retrying import retry
|
||||
|
||||
eventlet.monkey_patch(all=True)
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts([
|
||||
cfg.StrOpt('sysinv_api_bind_ip',
|
||||
default='0.0.0.0',
|
||||
help='IP for the Ceph Manager server to bind to')])
|
||||
CONF.logging_default_format_string = (
|
||||
'%(asctime)s.%(msecs)03d %(process)d '
|
||||
'%(levelname)s %(name)s [-] %(message)s')
|
||||
logging.register_options(CONF)
|
||||
logging.setup(CONF, __name__)
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF.rpc_backend = 'rabbit'
|
||||
|
||||
|
||||
class RpcEndpoint(PeriodicTasks):
|
||||
|
||||
def __init__(self, service=None):
|
||||
self.service = service
|
||||
|
||||
def cache_tiering_enable_cache(self, _, new_config, applied_config):
|
||||
LOG.info(_LI("Enabling cache"))
|
||||
try:
|
||||
self.service.cache_tiering.enable_cache(
|
||||
new_config, applied_config)
|
||||
except exception.CephManagerException as e:
|
||||
self.service.sysinv_conductor.call(
|
||||
{}, 'cache_tiering_enable_cache_complete',
|
||||
success=False, exception=str(e.message),
|
||||
new_config=new_config, applied_config=applied_config)
|
||||
|
||||
def cache_tiering_disable_cache(self, _, new_config, applied_config):
|
||||
LOG.info(_LI("Disabling cache"))
|
||||
try:
|
||||
self.service.cache_tiering.disable_cache(
|
||||
new_config, applied_config)
|
||||
except exception.CephManagerException as e:
|
||||
self.service.sysinv_conductor.call(
|
||||
{}, 'cache_tiering_disable_cache_complete',
|
||||
success=False, exception=str(e.message),
|
||||
new_config=new_config, applied_config=applied_config)
|
||||
|
||||
def cache_tiering_operation_in_progress(self, _):
|
||||
is_locked = self.service.cache_tiering.is_locked()
|
||||
LOG.info(_LI("Cache tiering operation "
|
||||
"is in progress: %s") % str(is_locked).lower())
|
||||
return is_locked
|
||||
|
||||
def get_primary_tier_size(self, _):
|
||||
"""Get the ceph size for the primary tier.
|
||||
|
||||
returns: an int for the size (in GB) of the tier
|
||||
"""
|
||||
|
||||
tiers_size = self.service.monitor.tiers_size
|
||||
primary_tier_size = tiers_size.get(
|
||||
self.service.monitor.primary_tier_name, 0)
|
||||
LOG.debug(_LI("Ceph cluster primary tier size: %s GB") %
|
||||
str(primary_tier_size))
|
||||
return primary_tier_size
|
||||
|
||||
def get_tiers_size(self, _):
|
||||
"""Get the ceph cluster tier sizes.
|
||||
|
||||
returns: a dict of sizes (in GB) by tier name
|
||||
"""
|
||||
|
||||
tiers_size = self.service.monitor.tiers_size
|
||||
LOG.debug(_LI("Ceph cluster tiers (size in GB): %s") %
|
||||
str(tiers_size))
|
||||
return tiers_size
|
||||
|
||||
def is_cluster_up(self, _):
|
||||
"""Report if the last health check was successful.
|
||||
|
||||
This is an independent view of the cluster accessibility that can be
|
||||
used by the sysinv conductor to gate ceph API calls which would timeout
|
||||
and potentially block other operations.
|
||||
|
||||
This view is only updated at the rate the monitor checks for a cluster
|
||||
uuid or a health check (CEPH_HEALTH_CHECK_INTERVAL)
|
||||
|
||||
returns: boolean True if last health check was successful else False
|
||||
"""
|
||||
return self.service.monitor.cluster_is_up
|
||||
|
||||
|
||||
# This class is needed only when upgrading from 16.10 to 17.x
|
||||
# TODO: remove it after 1st 17.x release
|
||||
#
|
||||
class SysinvConductorUpgradeApi(object):
|
||||
def __init__(self):
|
||||
self.sysinv_conductor = None
|
||||
super(SysinvConductorUpgradeApi, self).__init__()
|
||||
|
||||
def get_software_upgrade_status(self):
|
||||
LOG.info(_LI("Getting software upgrade status from sysinv"))
|
||||
cctxt = self.sysinv_conductor.prepare(timeout=2)
|
||||
upgrade = cctxt.call({}, 'get_software_upgrade_status')
|
||||
LOG.info(_LI("Software upgrade status: %s") % str(upgrade))
|
||||
return upgrade
|
||||
|
||||
@retry(wait_fixed=1000,
|
||||
retry_on_exception=lambda exception:
|
||||
LOG.warn(_LW(
|
||||
"Getting software upgrade status failed "
|
||||
"with: %s. Retrying... ") % str(exception)) or True)
|
||||
def retry_get_software_upgrade_status(self):
|
||||
return self.get_software_upgrade_status()
|
||||
|
||||
|
||||
class Service(SysinvConductorUpgradeApi, service.Service):
|
||||
|
||||
def __init__(self, conf):
|
||||
super(Service, self).__init__()
|
||||
self.conf = conf
|
||||
self.rpc_server = None
|
||||
self.sysinv_conductor = None
|
||||
self.ceph_api = None
|
||||
self.entity_instance_id = ''
|
||||
self.fm_api = fm_api.FaultAPIs()
|
||||
self.monitor = Monitor(self)
|
||||
self.cache_tiering = CacheTiering(self)
|
||||
self.config = None
|
||||
self.config_desired = None
|
||||
self.config_applied = None
|
||||
|
||||
def start(self):
|
||||
super(Service, self).start()
|
||||
transport = messaging.get_transport(self.conf)
|
||||
self.sysinv_conductor = messaging.RPCClient(
|
||||
transport,
|
||||
messaging.Target(
|
||||
topic=constants.SYSINV_CONDUCTOR_TOPIC))
|
||||
|
||||
self.ceph_api = wrapper.CephWrapper(
|
||||
endpoint='http://localhost:5001/api/v0.1/')
|
||||
|
||||
# Get initial config from sysinv and send it to
|
||||
# services that need it before starting them
|
||||
config = self.get_caching_tier_config()
|
||||
self.monitor.setup(config)
|
||||
self.rpc_server = messaging.get_rpc_server(
|
||||
transport,
|
||||
messaging.Target(topic=constants.CEPH_MANAGER_TOPIC,
|
||||
server=self.conf.sysinv_api_bind_ip),
|
||||
[RpcEndpoint(self)],
|
||||
executor='eventlet')
|
||||
self.rpc_server.start()
|
||||
self.cache_tiering.set_initial_config(config)
|
||||
eventlet.spawn_n(self.monitor.run)
|
||||
periodic = loopingcall.FixedIntervalLoopingCall(
|
||||
self.update_ceph_target_max_bytes)
|
||||
periodic.start(interval=300)
|
||||
|
||||
def get_caching_tier_config(self):
|
||||
LOG.info("Getting cache tiering configuration from sysinv")
|
||||
while True:
|
||||
# Get initial configuration from sysinv,
|
||||
# retry until sysinv starts
|
||||
try:
|
||||
cctxt = self.sysinv_conductor.prepare(timeout=2)
|
||||
config = cctxt.call({}, 'cache_tiering_get_config')
|
||||
for section in config:
|
||||
if section == constants.CACHE_TIERING:
|
||||
self.config = ServiceConfig().from_dict(
|
||||
config[section])
|
||||
elif section == constants.CACHE_TIERING_DESIRED:
|
||||
self.config_desired = ServiceConfig().from_dict(
|
||||
config[section])
|
||||
elif section == constants.CACHE_TIERING_APPLIED:
|
||||
self.config_applied = ServiceConfig().from_dict(
|
||||
config[section])
|
||||
LOG.info("Cache tiering configs: {}".format(config))
|
||||
return config
|
||||
except Exception as ex:
|
||||
# In production we should retry on every error until connection
|
||||
# is reestablished.
|
||||
LOG.warn("Getting cache tiering configuration failed "
|
||||
"with: {}. Retrying... ".format(str(ex)))
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
self.rpc_server.stop()
|
||||
self.rpc_server.wait()
|
||||
except Exception:
|
||||
pass
|
||||
super(Service, self).stop()
|
||||
|
||||
def update_ceph_target_max_bytes(self):
|
||||
try:
|
||||
self.cache_tiering.update_cache_target_max_bytes()
|
||||
except Exception as ex:
|
||||
LOG.exception("Updating Ceph target max bytes failed "
|
||||
"with: {} retrying on next cycle.".format(str(ex)))
|
||||
|
||||
|
||||
def run_service():
|
||||
CONF(sys.argv[1:])
|
||||
logging.setup(CONF, "ceph-manager")
|
||||
launcher = service.launch(CONF, Service(CONF), workers=1)
|
||||
launcher.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_service()
|
@ -0,0 +1,309 @@
|
||||
#
|
||||
# Copyright (c) 2016 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import unittest
|
||||
import mock
|
||||
|
||||
import subprocess
|
||||
import math
|
||||
|
||||
from ..cache_tiering import CacheTiering
|
||||
from ..cache_tiering import LOG as CT_LOG
|
||||
from ..constants import CACHE_FLUSH_OBJECTS_THRESHOLD
|
||||
from ..constants import CACHE_FLUSH_MIN_WAIT_OBJ_COUNT_DECREASE_SEC as MIN_WAIT
|
||||
from ..constants import CACHE_FLUSH_MAX_WAIT_OBJ_COUNT_DECREASE_SEC as MAX_WAIT
|
||||
from ..exception import CephCacheFlushFailure
|
||||
|
||||
|
||||
class TestCacheFlush(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.service = mock.Mock()
|
||||
self.ceph_api = mock.Mock()
|
||||
self.service.ceph_api = self.ceph_api
|
||||
self.cache_tiering = CacheTiering(self.service)
|
||||
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_set_param_fail(self, mock_proc_call):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=False, status_code=500, reason='denied'),
|
||||
{})
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_df_fail(self, mock_proc_call):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{})
|
||||
self.ceph_api.df = mock.Mock()
|
||||
self.ceph_api.df.return_value = (
|
||||
mock.Mock(ok=False, status_code=500, reason='denied'),
|
||||
{})
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
self.ceph_api.osd_set_pool_param.assert_called_once_with(
|
||||
'test-cache', 'target_max_objects', 1, force=None, body='json')
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_rados_evict_fail_raises(self, mock_proc_call):
|
||||
mock_proc_call.side_effect = subprocess.CalledProcessError(1, ['cmd'])
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=False, status_code=500, reason='denied'),
|
||||
{})
|
||||
self.assertRaises(CephCacheFlushFailure,
|
||||
self.cache_tiering.cache_flush,
|
||||
{'pool_name': 'test'})
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_df_missing_pool(self, mock_proc_call):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{})
|
||||
self.ceph_api.df = mock.Mock()
|
||||
self.ceph_api.df.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'rbd',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects': 0}}]},
|
||||
'status': 'OK'})
|
||||
with mock.patch.object(CT_LOG, 'warn') as mock_lw:
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
self.ceph_api.df.assert_called_once_with(body='json')
|
||||
for c in mock_lw.call_args_list:
|
||||
if 'Missing pool free space' in c[0][0]:
|
||||
break
|
||||
else:
|
||||
self.fail('expected log warning')
|
||||
self.ceph_api.osd_set_pool_param.assert_called_once_with(
|
||||
'test-cache', 'target_max_objects', 1, force=None, body='json')
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_df_objects_empty(self, mock_proc_call):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{})
|
||||
self.ceph_api.df = mock.Mock()
|
||||
self.ceph_api.df.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects': 0}}]},
|
||||
'status': 'OK'})
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
self.ceph_api.df.assert_called_once_with(body='json')
|
||||
self.ceph_api.osd_set_pool_param.assert_called_once_with(
|
||||
'test-cache', 'target_max_objects', 1, force=None, body='json')
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_df_objects_above_threshold(self, mock_proc_call, mock_time_sleep):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{})
|
||||
self.ceph_api.df = mock.Mock()
|
||||
self.ceph_api.df.side_effect = [
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects': CACHE_FLUSH_OBJECTS_THRESHOLD}}]},
|
||||
'status': 'OK'}),
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD - 1}}]},
|
||||
'status': 'OK'})]
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
self.ceph_api.osd_set_pool_param.assert_called_once_with(
|
||||
'test-cache', 'target_max_objects', 1, force=None, body='json')
|
||||
self.ceph_api.df.assert_called_with(body='json')
|
||||
mock_time_sleep.assert_called_once_with(MIN_WAIT)
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_df_objects_interval_increase(self, mock_proc_call,
|
||||
mock_time_sleep):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{})
|
||||
self.ceph_api.df = mock.Mock()
|
||||
self.ceph_api.df.side_effect = [
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD + 1}}]},
|
||||
'status': 'OK'}),
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD + 1}}]},
|
||||
'status': 'OK'}),
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD + 1}}]},
|
||||
'status': 'OK'}),
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD - 1}}]},
|
||||
'status': 'OK'})]
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
self.ceph_api.osd_set_pool_param.assert_called_once_with(
|
||||
'test-cache', 'target_max_objects', 1, force=None, body='json')
|
||||
self.ceph_api.df.assert_called_with(body='json')
|
||||
self.assertEqual([c[0][0] for c in mock_time_sleep.call_args_list],
|
||||
[MIN_WAIT,
|
||||
MIN_WAIT * 2,
|
||||
MIN_WAIT * 4])
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_df_objects_allways_over_threshold(self, mock_proc_call,
|
||||
mock_time_sleep):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{})
|
||||
self.ceph_api.df = mock.Mock()
|
||||
self.ceph_api.df.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD + 1}}]},
|
||||
'status': 'OK'})
|
||||
# noinspection PyTypeChecker
|
||||
mock_time_sleep.side_effect = \
|
||||
[None]*int(math.ceil(math.log(float(MAX_WAIT)/MIN_WAIT, 2)) + 1) \
|
||||
+ [Exception('too many sleeps')]
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
self.ceph_api.osd_set_pool_param.assert_called_once_with(
|
||||
'test-cache', 'target_max_objects', 1, force=None, body='json')
|
||||
self.ceph_api.df.assert_called_with(body='json')
|
||||
expected_sleep = []
|
||||
interval = MIN_WAIT
|
||||
while interval <= MAX_WAIT:
|
||||
expected_sleep.append(interval)
|
||||
interval *= 2
|
||||
self.assertEqual([c[0][0] for c in mock_time_sleep.call_args_list],
|
||||
expected_sleep)
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
@mock.patch('subprocess.check_call')
|
||||
def test_df_objects_increase(self, mock_proc_call, mock_time_sleep):
|
||||
self.ceph_api.osd_set_pool_param = mock.Mock()
|
||||
self.ceph_api.osd_set_pool_param.return_value = (
|
||||
mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{})
|
||||
self.ceph_api.df = mock.Mock()
|
||||
self.ceph_api.df.side_effect = [
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD + 1}}]},
|
||||
'status': 'OK'}),
|
||||
(mock.Mock(ok=True, status_code=200, reason='OK'),
|
||||
{'output': {
|
||||
'pools': [
|
||||
{'id': 0,
|
||||
'name': 'test-cache',
|
||||
'stats': {'bytes_used': 0,
|
||||
'kb_used': 0,
|
||||
'max_avail': 9588428800,
|
||||
'objects':
|
||||
CACHE_FLUSH_OBJECTS_THRESHOLD + 2}}]},
|
||||
'status': 'OK'})]
|
||||
with mock.patch.object(CT_LOG, 'warn') as mock_lw:
|
||||
self.cache_tiering.cache_flush({'pool_name': 'test'})
|
||||
for c in mock_lw.call_args_list:
|
||||
if 'Unexpected increase' in c[0][0]:
|
||||
break
|
||||
else:
|
||||
self.fail('expected log warning')
|
||||
self.ceph_api.df.assert_called_with(body='json')
|
||||
mock_time_sleep.assert_called_once_with(MIN_WAIT)
|
||||
self.ceph_api.osd_set_pool_param.assert_called_once_with(
|
||||
'test-cache', 'target_max_objects', 1, force=None, body='json')
|
||||
mock_proc_call.assert_called_with(
|
||||
['/usr/bin/rados', '-p', 'test-cache', 'cache-flush-evict-all'])
|
19
ceph/ceph-manager/ceph-manager/setup.py
Normal file
19
ceph/ceph-manager/ceph-manager/setup.py
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2013-2014, 2016 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
name='ceph_manager',
|
||||
version='1.0.0',
|
||||
description='CEPH manager',
|
||||
license='Apache-2.0',
|
||||
packages=['ceph_manager'],
|
||||
entry_points={
|
||||
}
|
||||
)
|
10
ceph/ceph-manager/ceph-manager/test-requirements.txt
Normal file
10
ceph/ceph-manager/ceph-manager/test-requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
mock
|
||||
flake8
|
||||
eventlet
|
||||
pytest
|
||||
oslo.log
|
||||
oslo.i18n
|
29
ceph/ceph-manager/ceph-manager/tox.ini
Normal file
29
ceph/ceph-manager/ceph-manager/tox.ini
Normal file
@ -0,0 +1,29 @@
|
||||
# adapted from glance tox.ini
|
||||
|
||||
[tox]
|
||||
minversion = 1.6
|
||||
envlist = py27,pep8
|
||||
skipsdist = True
|
||||
# tox does not work if the path to the workdir is too long, so move it to /tmp
|
||||
toxworkdir = /tmp/{env:USER}_ceph_manager_tox
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install --no-use-wheel -U --force-reinstall {opts} {packages}
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = py.test {posargs}
|
||||
whitelist_externals = bash
|
||||
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
|
||||
|
||||
[testenv:py27]
|
||||
basepython = python2.7
|
||||
setenv =
|
||||
PYTHONPATH={toxinidir}/../../../../sysinv/recipes-common/sysinv/sysinv:{toxinidir}/../../../../config/recipes-common/tsconfig/tsconfig
|
||||
|
||||
[testenv:pep8]
|
||||
commands =
|
||||
flake8 {posargs}
|
||||
|
||||
[flake8]
|
||||
exclude = .venv,.git,.tox,dist,doc,etc,*glance/locale*,*lib/python*,*egg,build
|
11
ceph/ceph-manager/files/ceph-manager.logrotate
Normal file
11
ceph/ceph-manager/files/ceph-manager.logrotate
Normal file
@ -0,0 +1,11 @@
|
||||
/var/log/ceph-manager.log {
|
||||
nodateext
|
||||
size 10M
|
||||
start 1
|
||||
rotate 10
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
}
|
17
ceph/ceph-manager/files/ceph-manager.service
Normal file
17
ceph/ceph-manager/files/ceph-manager.service
Normal file
@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Handle Ceph API calls and provide status updates via alarms
|
||||
After=ceph.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
Restart=no
|
||||
KillMode=process
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/etc/rc.d/init.d/ceph-manager start
|
||||
ExecStop=/etc/rc.d/init.d/ceph-manager stop
|
||||
ExecReload=/etc/rc.d/init.d/ceph-manager reload
|
||||
PIDFile=/var/run/ceph/ceph-manager.pid
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
17
ceph/ceph-manager/scripts/bin/ceph-manager
Normal file
17
ceph/ceph-manager/scripts/bin/ceph-manager
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2016 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
from ceph_manager.server import run_service
|
||||
except EnvironmentError as e:
|
||||
print >> sys.stderr, "Error importing ceph_manager: ", str(e)
|
||||
sys.exit(1)
|
||||
|
||||
run_service()
|
103
ceph/ceph-manager/scripts/init.d/ceph-manager
Normal file
103
ceph/ceph-manager/scripts/init.d/ceph-manager
Normal file
@ -0,0 +1,103 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2013-2014, 2016 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: ceph-manager
|
||||
# Required-Start: $ceph
|
||||
# Required-Stop: $ceph
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Daemon for polling ceph status
|
||||
# Description: Daemon for polling ceph status
|
||||
### END INIT INFO
|
||||
|
||||
DESC="ceph-manager"
|
||||
DAEMON="/usr/bin/ceph-manager"
|
||||
RUNDIR="/var/run/ceph"
|
||||
PIDFILE=$RUNDIR/$DESC.pid
|
||||
|
||||
CONFIGFILE="/etc/sysinv/sysinv.conf"
|
||||
LOGFILE="/var/log/ceph-manager.log"
|
||||
|
||||
start()
|
||||
{
|
||||
if [ -e $PIDFILE ]; then
|
||||
PIDDIR=/prod/$(cat $PIDFILE)
|
||||
if [ -d ${PIDFILE} ]; then
|
||||
echo "$DESC already running."
|
||||
exit 0
|
||||
else
|
||||
echo "Removing stale PID file $PIDFILE"
|
||||
rm -f $PIDFILE
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -n "Starting $DESC..."
|
||||
mkdir -p $RUNDIR
|
||||
start-stop-daemon --start --quiet \
|
||||
--pidfile ${PIDFILE} --exec ${DAEMON} \
|
||||
--make-pidfile --background \
|
||||
-- --log-file=$LOGFILE --config-file=$CONFIGFILE
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "done."
|
||||
else
|
||||
echo "failed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
echo -n "Stopping $DESC..."
|
||||
start-stop-daemon --stop --quiet --pidfile $PIDFILE --retry 60
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "done."
|
||||
else
|
||||
echo "failed."
|
||||
fi
|
||||
rm -f $PIDFILE
|
||||
}
|
||||
|
||||
status()
|
||||
{
|
||||
pid=`cat $PIDFILE 2>/dev/null`
|
||||
if [ -n "$pid" ]; then
|
||||
if ps -p $pid &> /dev/null ; then
|
||||
echo "$DESC is running"
|
||||
exit 0
|
||||
else
|
||||
echo "$DESC is not running but has pid file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "$DESC is not running"
|
||||
exit 3
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart|force-reload|reload)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|force-reload|restart|reload|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
Loading…
Reference in New Issue
Block a user