2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-09-29 00:31:09 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Jarrod Johnson 123a002b6c Add expressions to Operator role
noderun/nodeshell would not work for operators without this.
2019-05-03 09:04:10 -04:00
Jarrod Johnson 2721f2bd7b Add Monitor role
Add a monitor role that is only viable for monitoring relevant
tasks.
2019-05-02 13:18:49 -04:00
Jarrod Johnson 43f26d8cbb Add support for Operator role
Support a reduced privilege user that can still perform
most operations, but cannot modify, delete, or add
users/groups to confluent or to BMCs.
2019-05-02 13:18:43 -04:00
799 changed files with 5354 additions and 101447 deletions
-127
View File
@@ -1,127 +0,0 @@
name: CI
on:
push:
pull_request:
permissions:
contents: read
jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run ShellCheck (errors only)
# Check every tracked file that has a .sh extension or an sh/bash
# shebang. SC2148 (missing shebang) is excluded because many .sh
# files are sourced fragments or dracut hooks; ShellCheck then
# falls back to checking them as bash.
run: |
shebang_re='^#!.*[/ ](sh|bash|dash|ash|ksh)([[:blank:]]|$)'
{
git ls-files '*.sh'
git ls-files | while IFS= read -r f; do
[ -f "$f" ] || continue
firstline=
# An empty file makes read fail, which under -e would end the run.
IFS= read -r -n 200 firstline < "$f" 2>/dev/null || true
if [[ $firstline =~ $shebang_re ]]; then
printf '%s\n' "$f"
fi
done
} | sort -u > /tmp/shfiles
# A selection that quietly comes up empty would check nothing and
# still pass, so say how many files there are and insist on some.
echo "$(wc -l < /tmp/shfiles) shell files"
[ -s /tmp/shfiles ] || { echo '::error::No shell files found'; exit 1; }
xargs -d '\n' shellcheck --severity=error --exclude=SC2148 < /tmp/shfiles
ruff:
name: Ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Pinned to an exact release: unlike actions/checkout, ruff-action
# publishes no moving major tag past v3, so @v4 does not resolve.
- uses: astral-sh/ruff-action@v4.1.0
with:
version: latest
# Rule selection, file discovery (the many extensionless Python
# executables) and exclusions all live in ruff.toml, so the whole
# workspace can be handed over as-is.
args: check --output-format=github
pyrefly:
name: Pyrefly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: facebook/pyrefly@main
python-compileall:
name: Python compileall
runs-on: ubuntu-latest
env:
# One entry per Python version shipped by the distros confluent
# targets, limited to versions actions/setup-python still provides
# on current runners (sles15/alma8 ship 3.6, which is unavailable).
# Newline-separated so it feeds both setup-python (multiline input)
# and the shell loop below (word-split on whitespace).
PYTHON_VERSIONS: |
3.8
3.9
3.10
3.12
3.13
3.14
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSIONS }}
- name: List the Python files without a .py name
# compileall only ever compiles *.py: handed anything else, even by
# name, it skips it and still exits 0. That leaves every extensionless
# CLI tool, deploy script and setup.py.tmpl unchecked, so collect them
# here and feed them to py_compile, which does compile what it is
# given. The first line is read with the shell builtin rather than
# forking head and grep per file.
run: |
shebang_re='^#!.*python'
{
git ls-files | while IFS= read -r f; do
[ -f "$f" ] || continue
case "$f" in *.py) continue ;; esac
firstline=
# An empty file makes read fail, which under -e would end the run.
IFS= read -r -n 200 firstline < "$f" 2>/dev/null || true
if [[ $firstline =~ $shebang_re ]]; then
printf '%s\n' "$f"
fi
done
# Python that carries no shebang at all, so nothing can detect it.
# Kept in step with extend-include in ruff.toml.
git ls-files '*/setup.py.tmpl' '*/scripts/configbmc' \
'*/scripts/add_local_repositories' 'misc/filterpasswd'
} | sort -u > /tmp/pyfiles
# An empty list would leave py_compile with nothing to do and the
# job green, which is the very hole this step exists to close.
echo "$(wc -l < /tmp/pyfiles) files without a .py name"
[ -s /tmp/pyfiles ] || { echo '::error::No such files found'; exit 1; }
- name: Compile all Python files
run: |
rc=0
for v in $PYTHON_VERSIONS; do
echo "::group::Python $v"
ok=0
"python$v" -W error -m compileall -q -x '/\.git/' . || ok=1
xargs -d '\n' "python$v" -W error -m py_compile < /tmp/pyfiles || ok=1
echo "::endgroup::"
if [ "$ok" -ne 0 ]; then
echo "::error::Python $v compileall failed"
rc=1
fi
done
exit "$rc"
-19
View File
@@ -1,19 +0,0 @@
name: Create Release
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Create Release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
+1 -7
View File
@@ -1,7 +1 @@
*.pyc
.*.
confluent_client/man/man*
.vscode
.*.sw*
.sw*
.idea/*
.idea
+25
View File
@@ -175,3 +175,28 @@
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.
-50
View File
@@ -1,50 +0,0 @@
# Confluent
![Python 3](https://img.shields.io/badge/python-3-blue.svg) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/xcat2/confluent/blob/master/LICENSE)
Confluent is a software package to handle essential bootstrap and operation of scale-out server configurations.
It supports stateful and stateless deployments for various operating systems.
Check [this page](https://xcat2.github.io/confluent-docs/) for a more detailed list of features.
Confluent is the modern successor of [xCAT](https://github.com/xcat2/xcat-core).
If you're coming from xCAT, check out [this comparison](https://xcat2.github.io/confluent-docs/miscellaneous/confluentvxcat/).
# Documentation
Confluent documentation is hosted on: https://xcat2.github.io/confluent-docs/
# Download
Get the latest version from: https://xcat2.github.io/confluent-docs/downloads/
Check release notes on: https://xcat2.github.io/confluent-docs/release_notes/
# Open Source License
Confluent is made available under the Apache 2.0 license: https://opensource.org/license/apache-2-0
# Developers
Want to help? Submit a [Pull Request](https://github.com/xcat2/confluent/pulls).
## Versioning and releases
The top-level `VERSION` file names the release the current branch is working toward. Build scripts
call `./mkversion`, which turns it into the version stamped on packages: the tag itself on a release
tag (`4.0.1`), otherwise a development version (`4.0.1.dev5+gdeadbee`, or `4.0.1~dev5+gdeadbee` for
the packages that have no `setup.py`).
`mkversion` also derives a version from the newest tag reachable from HEAD and uses whichever is
higher, so forgetting to bump `VERSION` after tagging the current branch cannot walk the version
backwards. Staying ahead of tags on *other* branches is what `VERSION` itself is for: patch releases
are tagged on release branches, which master never sees.
Cutting a new series:
1. Land the release commit on master and tag `X.Y.0` there.
2. Create branch `X.Y` from that tag; it inherits `VERSION=X.Y.0`.
3. Only then bump `VERSION` on master. Doing it before step 2 leaves the release branch on the
wrong series.
Patch releases land on branch `X.Y` and are tagged `X.Y.Z` there; no `VERSION` edit is needed.
-1
View File
@@ -1 +0,0 @@
4.0.0
-8
View File
@@ -1,8 +0,0 @@
FROM almalinux:10
RUN ["yum", "-y","update"]
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-XML-LibXML", "pinentry-tty", "rpm-sign", "git", "golang"]
ADD rpmmacro /root/.rpmmacros
ADD buildpackages.sh /bin/
#VOLUME ["/rpms", "/srpms"]
CMD ["/bin/bash","/bin/buildpackages.sh"]
-6
View File
@@ -1,6 +0,0 @@
for package in /srpms/*; do
rpmbuild --rebuild $package
done
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
-3
View File
@@ -1,3 +0,0 @@
%_gpg_digest_algo sha256
%_gpg_name Lenovo Scalable Infrastructure
-8
View File
@@ -1,8 +0,0 @@
FROM almalinux:8
RUN ["yum", "-y","update"]
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-Net-DNS", "perl-DB_File", "perl-XML-LibXML", "rpm-sign", "git", "fuse-devel","libcurl-devel"]
ADD rpmmacro /root/.rpmmacros
ADD buildpackages.sh /bin/
#VOLUME ["/rpms", "/srpms"]
CMD ["/bin/bash","/bin/buildpackages.sh"]
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
for package in /srpms/*; do
rpmbuild --rebuild $package
done
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
-2
View File
@@ -1,2 +0,0 @@
%_gpg_digest_algo sha256
%_gpg_name Lenovo Scalable Infrastructure
-10
View File
@@ -1,10 +0,0 @@
FROM almalinux:9
RUN ["yum", "-y","update"]
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-Net-DNS", "perl-DB_File", "perl-XML-LibXML", "pinentry-tty", "rpm-sign", "epel-release", "git"]
RUN ["crb", "enable"]
RUN ["yum", "-y","install","fuse-devel","libcurl-devel"]
ADD rpmmacro /root/.rpmmacros
ADD buildpackages.sh /bin/
#VOLUME ["/rpms", "/srpms"]
CMD ["/bin/bash","/bin/buildpackages.sh"]
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
for package in /srpms/*; do
rpmbuild --rebuild $package
done
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
-2
View File
@@ -1,2 +0,0 @@
%_gpg_digest_algo sha256
%_gpg_name Lenovo Scalable Infrastructure
-12
View File
@@ -1,12 +0,0 @@
FROM ubuntu:noble
ADD stdeb.patch /tmp/
ADD buildapt.sh /bin/
ADD distributions.tmpl /bin/
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "reprepro", "python3-stdeb", "gnupg-agent", "devscripts", "debhelper", "libsoap-lite-perl", "libdbi-perl", "quilt", "git", "python3-pyparsing", "python3-netifaces", "dh-python", "libjson-perl", "pandoc", "alien", "gcc", "make", "alien"]
RUN ["mkdir", "-p", "/sources/git/"]
RUN ["mkdir", "-p", "/debs/"]
RUN ["mkdir", "-p", "/apt/"]
RUN ["bash", "-c", "patch -p1 < /tmp/stdeb.patch"]
CMD ["/bin/bash", "/bin/buildapt.sh"]
-21
View File
@@ -1,21 +0,0 @@
#cp -a /sources/git /tmp
for builder in $(find /sources/git -name builddeb); do
cd $(dirname $builder)
./builddeb /debs/
done
cp /prebuilt/* /debs/
cp /osd/*.deb /debs/
mkdir -p /apt/conf/
CODENAME=$(grep VERSION_CODENAME= /etc/os-release | sed -e 's/.*=//')
if [ -z "$CODENAME" ]; then
CODENAME=$(grep VERSION= /etc/os-release | sed -e 's/.*(//' -e 's/).*//')
fi
if ! grep $CODENAME /apt/conf/distributions; then
sed -e s/#CODENAME#/$CODENAME/ /bin/distributions.tmpl >> /apt/conf/distributions
fi
cd /apt/
reprepro includedeb $CODENAME /debs/*.deb
for dsc in /debs/*.dsc; do
reprepro includedsc $CODENAME $dsc
done
-7
View File
@@ -1,7 +0,0 @@
Origin: Lenovo HPC Packages
Label: Lenovo HPC Packages
Codename: #CODENAME#
Architectures: amd64 source
Components: main
Description: Lenovo HPC Packages
-34
View File
@@ -1,34 +0,0 @@
diff -urN t/usr/lib/python3/dist-packages/stdeb/cli_runner.py t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py
--- t/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:30:13.930328999 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:32:05.392731405 +0000
@@ -8,7 +8,7 @@
from ConfigParser import SafeConfigParser # noqa: F401
except ImportError:
# python 3.x
- from configparser import SafeConfigParser # noqa: F401
+ from configparser import ConfigParser # noqa: F401
from distutils.util import strtobool
from distutils.fancy_getopt import FancyGetopt, translate_longopt
from stdeb.util import stdeb_cmdline_opts, stdeb_cmd_bool_opts
diff -urN t/usr/lib/python3/dist-packages/stdeb/util.py t.patch/usr/lib/python3/dist-packages/stdeb/util.py
--- t/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:32:53.864776149 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:33:02.063952870 +0000
@@ -730,7 +730,7 @@
example.
"""
- cfg = ConfigParser.SafeConfigParser()
+ cfg = ConfigParser.ConfigParser()
cfg.read(cfg_files)
if cfg.has_section(module_name):
section_items = cfg.items(module_name)
@@ -801,7 +801,7 @@
if len(cfg_files):
check_cfg_files(cfg_files, module_name)
- cfg = ConfigParser.SafeConfigParser(cfg_defaults)
+ cfg = ConfigParser.ConfigParser(cfg_defaults)
for cfg_file in cfg_files:
with codecs.open(cfg_file, mode='r', encoding='utf-8') as fd:
cfg.readfp(fd)
-11
View File
@@ -1,11 +0,0 @@
FROM ubuntu:resolute
ADD stdeb.patch /tmp/
ADD buildapt.sh /bin/
ADD distributions.tmpl /bin/
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "reprepro", "python3-stdeb", "gnupg-agent", "devscripts", "debhelper", "libsoap-lite-perl", "libdbi-perl", "quilt", "git", "python3-pyparsing", "python3-dnspython", "python3-netifaces", "python3-asyncssh", "dh-python", "libjson-perl", "pandoc", "alien", "gcc", "make"]
RUN ["mkdir", "-p", "/sources/git/"]
RUN ["mkdir", "-p", "/debs/"]
RUN ["mkdir", "-p", "/apt/"]
CMD ["/bin/bash", "/bin/buildapt.sh"]
-21
View File
@@ -1,21 +0,0 @@
#cp -a /sources/git /tmp
for builder in $(find /sources/git -name builddeb); do
cd $(dirname $builder)
./builddeb /debs/
done
cp /prebuilt/* /debs/
cp /osd/*.deb /debs/
mkdir -p /apt/conf/
CODENAME=$(grep VERSION_CODENAME= /etc/os-release | sed -e 's/.*=//')
if [ -z "$CODENAME" ]; then
CODENAME=$(grep VERSION= /etc/os-release | sed -e 's/.*(//' -e 's/).*//')
fi
if ! grep $CODENAME /apt/conf/distributions; then
sed -e s/#CODENAME#/$CODENAME/ /bin/distributions.tmpl >> /apt/conf/distributions
fi
cd /apt/
reprepro includedeb $CODENAME /debs/*.deb
for dsc in /debs/*.dsc; do
reprepro includedsc $CODENAME $dsc
done
-7
View File
@@ -1,7 +0,0 @@
Origin: Lenovo HPC Packages
Label: Lenovo HPC Packages
Codename: #CODENAME#
Architectures: amd64 source
Components: main
Description: Lenovo HPC Packages
-34
View File
@@ -1,34 +0,0 @@
diff -urN t/usr/lib/python3/dist-packages/stdeb/cli_runner.py t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py
--- t/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:30:13.930328999 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:32:05.392731405 +0000
@@ -8,7 +8,7 @@
from ConfigParser import SafeConfigParser # noqa: F401
except ImportError:
# python 3.x
- from configparser import SafeConfigParser # noqa: F401
+ from configparser import ConfigParser # noqa: F401
from distutils.util import strtobool
from distutils.fancy_getopt import FancyGetopt, translate_longopt
from stdeb.util import stdeb_cmdline_opts, stdeb_cmd_bool_opts
diff -urN t/usr/lib/python3/dist-packages/stdeb/util.py t.patch/usr/lib/python3/dist-packages/stdeb/util.py
--- t/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:32:53.864776149 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:33:02.063952870 +0000
@@ -730,7 +730,7 @@
example.
"""
- cfg = ConfigParser.SafeConfigParser()
+ cfg = ConfigParser.ConfigParser()
cfg.read(cfg_files)
if cfg.has_section(module_name):
section_items = cfg.items(module_name)
@@ -801,7 +801,7 @@
if len(cfg_files):
check_cfg_files(cfg_files, module_name)
- cfg = ConfigParser.SafeConfigParser(cfg_defaults)
+ cfg = ConfigParser.ConfigParser(cfg_defaults)
for cfg_file in cfg_files:
with codecs.open(cfg_file, mode='r', encoding='utf-8') as fd:
cfg.readfp(fd)
-9
View File
@@ -1,9 +0,0 @@
cd ~/confluent
git pull
rm ~/rpmbuild/RPMS/noarch/*osdeploy*
rm ~/rpmbuild/SRPMS/*osdeploy*
sh confluent_osdeploy/buildrpm-aarch64
mkdir -p $HOME/el9/
mkdir -p $HOME/el10/
podman run --rm -it -v $HOME:/build el9build bash /build/confluent/confluent_vtbufferd/buildrpm /build/el9/
-77
View File
@@ -1,77 +0,0 @@
Project: https://hpc.lenovo.com/users/
Source: https://github.com/lenovo/confluent
Upstream-Name: confluent-client
All file of the Confluent-client software is distributed under the terms of an Apache-2.0 license as indicated below:
Files: *
Copyright: 2014-2019 Lenovo
License: Apache-2.0
File: client.py:
Copyright: 2014 IBM Corporation
2015-2019 Lenovo
License: Apache-2.0
File: sortutil.py,
tlvdata.py
Copyright: 2014 IBM Corporation
2015-2016 Lenovo
License: Apache-2.0
File: tlv.py
Copyright: 2014 IBM Corporation
License: Apache-2.0
License: Apache-2.0
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:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
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
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
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/python
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
try:
sys.path.remove(path)
except Exception:
pass
path = os.path.realpath(os.path.join(path, '..', 'confluent_server'))
sys.path.append(path)
import confluent.config.attributes as attr
import shutil
shutil.copyfile('doc/man/nodeattrib.ronn.tmpl', 'doc/man/nodeattrib.ronn')
shutil.copyfile('doc/man/nodegroupattrib.ronn.tmpl', 'doc/man/nodegroupattrib.ronn')
def append_attributes(filename):
with open(filename, 'a') as outf:
for field in sorted(attr.node):
outf.write('\n* `{0}`:\n {1}\n'.format(field, attr.node[field]['description']))
# Optionally write valid values if they exist
for key, values in attr.node[field].items():
if key.startswith('valid'):
values_formatted = ', '.join("'{0}'".format(v) for v in values)
outf.write(f'\n Valid values: {values_formatted}\n')
append_attributes('doc/man/nodeattrib.ronn')
append_attributes('doc/man/nodegroupattrib.ronn')
+6 -41
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -21,10 +21,8 @@
import optparse
import os
import re
import select
import sys
import csv
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
@@ -45,15 +43,11 @@ argparser.add_option('-d', '--diff', action='store_true',
'output group and others')
argparser.add_option('-w', '--watch', action='store_true',
help='Show intermediate results while running')
argparser.add_option('-g', '--groupcount', action='store_true',
help='Show count of output groups rather than the actual output')
argparser.add_option('-s', '--skipcommon', action='store_true',
help='Do not print most common result, only non modal '
'groups, useful when combined with -d')
argparser.add_option('-c', '--count', action='store_true',
help='Also display count of nodes in a given group')
argparser.add_option('-t', '--tabular', action='store_true',
help='Output in CSV format')
argparser.add_option('-r', '--reverse', action='store_true',
help='Reverse sort order to show biggest output group '
'last')
@@ -71,30 +65,10 @@ if options.abbreviate:
else:
grouped = tg.GroupedData()
def print_current(tabular=False):
def print_current():
if options.diff:
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
reverse=options.reverse, basenode=options.base)
elif options.groupcount:
grouped.generate_byoutput()
print(len(grouped.byoutput))
elif tabular:
databynode = {}
fields = ['node']
for n in grouped.bynode:
for ln in grouped.bynode[n]:
label, value = ln.split(':', 1)
label = label.strip()
value = value.strip()
databynode.setdefault(n, {})[label] = value
if label not in fields:
fields.append(label)
writer = csv.DictWriter(sys.stdout, fieldnames=fields)
writer.writeheader()
for n in databynode:
row = dict(databynode[n])
row['node'] = n
writer.writerow(row)
else:
grouped.print_all(skipmodal=options.skipcommon,
count=options.count,
@@ -105,28 +79,19 @@ fullline = sys.stdin.readline()
printpending = True
clearpending = False
holdoff = 0
padded = None
while fullline:
for line in fullline.split('\n'):
if not line:
continue
if ':' not in line:
if ': ' not in line:
line = 'UNKNOWN: ' + line
if options.log:
node, output = line.split(':', 1)
if padded is None:
if output.startswith(' '):
padded = True
else:
padded = False
if padded:
output = re.sub(r'^ ', '', output)
node, output = line.split(': ', 1)
currlog = options.log.format(node=node, nodename=node)
with open(currlog, mode='a') as log:
log.write(output + '\n')
continue
node, output = line.split(':', 1)
grouped.add_line(node, output)
grouped.add_line(*line.split(': ', 1))
if options.watch:
if not holdoff:
holdoff = os.times()[4] + 0.250
@@ -145,5 +110,5 @@ while fullline:
if printpending:
if clearpending:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current(options.tabular)
print_current()
+124 -407
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
@@ -41,10 +41,10 @@
# esc-( would interfere with normal esc use too much
# ~ I will not use for now...
import math
import getpass
import optparse
import os
import re
import select
import shlex
import signal
@@ -76,11 +76,6 @@ import confluent.termhandler as termhandler
import confluent.tlvdata as tlvdata
import confluent.client as client
try:
unicode
except NameError:
unicode = str
conserversequence = '\x05c' # ctrl-e, c
clearpowermessage = False
@@ -94,10 +89,6 @@ except NameError:
netserver = None
laststate = {}
try:
input = raw_input
except NameError:
pass
class BailOut(Exception):
def __init__(self, errorcode=0):
@@ -107,8 +98,8 @@ class BailOut(Exception):
def print_help():
print("confetty provides a filesystem like interface to confluent. "
"Navigation is done using the same commands as would be used in a "
"filesystem. Tab completion is supported to aid in navigation, "
"as is up arrow to recall previous commands and control-r to search "
"filesystem. Tab completion is supported to aid in navigation,"
"as is up arrow to recall previous commands and control-r to search"
"previous command history, similar to using bash\n\n"
"The supported commands are:\n"
"cd [location] - Set the current command context, similar to a "
@@ -130,19 +121,8 @@ def print_help():
#common with the api document
def writeout(data):
while True:
try:
select.select((), (sys.stdout,), ())
sys.stdout.write(data)
break
except BlockingIOError:
continue
def updatestatus(stateinfo={}):
global powerstate, powertime, clearpowermessage
if opts.headless:
return
status = consolename
info = []
for statekey in stateinfo:
@@ -160,10 +140,10 @@ def updatestatus(stateinfo={}):
if 'state' in stateinfo: # currently only read power means anything
newpowerstate = stateinfo['state']['value']
if newpowerstate != powerstate and newpowerstate == 'off':
writeout("\x1b[2J\x1b[;H[powered off]\r\n")
sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n")
clearpowermessage = True
if newpowerstate == 'on' and clearpowermessage:
writeout("\x1b[2J\x1b[;H")
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
powerstate = newpowerstate
if 'clientcount' in laststate and laststate['clientcount'] != 1:
@@ -181,7 +161,7 @@ def updatestatus(stateinfo={}):
if info:
status += ' [' + ','.join(info) + ']'
if os.environ.get('TERM', '') not in ('linux'):
writeout('\x1b]0;console: %s\x07' % status)
sys.stdout.write('\x1b]0;console: %s\x07' % status)
sys.stdout.flush()
@@ -215,9 +195,9 @@ def prompt():
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;confetty: %s\x07' % target)
try:
return input(target + ' -> ')
return raw_input(target + ' -> ')
except KeyboardInterrupt:
print("")
print ""
return ""
except EOFError: # ctrl-d
print("exit")
@@ -303,8 +283,6 @@ def rcompleter(text, state):
def parse_command(command):
if isinstance(command, list):
return command
try:
args = shlex.split(command, posix=True)
except ValueError as ve:
@@ -317,11 +295,8 @@ currchildren = None
def print_result(res):
global exitcode
if 'errorcode' in res or 'error' in res:
print(res['error'])
if 'errorcode' in res:
exitcode |= res['errorcode']
print res['error']
return
if 'databynode' in res:
print_result(res['databynode'])
@@ -334,9 +309,9 @@ def print_result(res):
attrstr = '%s=%s' % (key, recurse_format(res[key]))
elif not isinstance(res[key], dict):
try:
print('{0}: {1}'.format(key, res[key]))
print '{0}: {1}'.format(key, res[key])
except UnicodeEncodeError:
print('{0}: {1}'.format(key, repr(res[key])))
print '{0}: {1}'.format(key, repr(res[key]))
continue
elif 'value' in res[key] and res[key]['value'] is not None:
attrstr = '%s="%s"' % (key, res[key]['value'])
@@ -349,7 +324,7 @@ def print_result(res):
else:
sys.stdout.write('{0}: '.format(key))
if isinstance(res[key], str) or isinstance(res[key], unicode):
print(res[key])
print res[key]
else:
print_result(res[key])
continue
@@ -384,7 +359,7 @@ def do_command(command, server):
if argv[0] == 'exit':
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
raise BailOut()
raise Bailout()
elif argv[0] in ('help', '?'):
return print_help()
elif argv[0] == 'cd':
@@ -448,10 +423,10 @@ def do_command(command, server):
for res in session.read(targpath):
if 'item' in res: # a link relation
if type(res['item']) == dict:
print(res['item']["href"])
print res['item']["href"]
else:
for item in res['item']:
print(item["href"])
print item["href"]
else: # generic attributes to list
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
@@ -461,14 +436,12 @@ def do_command(command, server):
print_result(res)
elif argv[0] == 'start':
targpath = fullpath_target(argv[1])
nodename = targpath.split('/')[2]
nodename = targpath.split('/')[-3]
currconsole = targpath
startrequest = {'operation': 'start', 'path': targpath,
'parameters': {}}
height, width = 31, 100
if not opts.headless:
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'........')[:4])[:2]
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
startrequest['parameters']['width'] = width
startrequest['parameters']['height'] = height
for param in argv[2:]:
@@ -531,13 +504,6 @@ def makecall(callout, args):
if 'errorcode' in response:
exitcode = response['errorcode']
sys.stderr.write('Error: ' + response['error'] + '\n')
if 'databynode' in response:
lresponse = response['databynode']
for node in lresponse:
if 'errorcode' in lresponse[node]:
exitcode = lresponse[node]['errorcode']
if 'error' in lresponse[node]:
sys.stderr.write('{0}: Error - {1}\n'.format(node, lresponse[node]['error']))
def clearvalues(resource, attribs):
@@ -624,7 +590,7 @@ def do_resize(a, b):
if not inconsole:
return
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'........')[:4])[:2]
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
tlvdata.send(session.connection, {'operation': 'resize', 'width': width,
'height': height})
@@ -636,21 +602,19 @@ def startconsole(nodename):
signal.signal(signal.SIGWINCH, do_resize)
didconsole = True
consolename = nodename
if not opts.headless:
tty.setraw(sys.stdin.fileno())
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
tty.setraw(sys.stdin.fileno())
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
inconsole = True
check_automation('') # give any leading 'sends' a chance
def quitconfetty(code=0, fullexit=False, fixterm=True):
global inconsole
global currconsole
global didconsole
if (fixterm or didconsole) and not opts.headless:
if fixterm or didconsole:
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl & ~os.O_NONBLOCK)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK)
if oldtcattr is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
# Request default color scheme, to undo potential weirdness of terminal
@@ -669,42 +633,15 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
def get_session_node(shellargs):
# straight to node console
if len(shellargs) == 1 and ' ' not in shellargs[0]:
targ = "/nodes/%s/console/session" % shellargs[0]
return targ, shellargs[0]
return shellargs[0]
if len(shellargs) == 2 and shellargs[0] == 'start':
args = [s for s in shellargs[1].split('/') if s]
if len(args) == 4 and args[0] == 'nodes':
if args[2] == 'console' and \
args[3] == 'session':
return shellargs[1], args[1]
if len(args) == 5 and args[0] == 'nodes' and args[2] == 'shell' and \
args[3] == 'sessions':
return shellargs[1], args[1]
return None, None
if len(args) == 4 and args[0] == 'nodes' and args[2] == 'console' and \
args[3] == 'session':
return args[1]
return None
def run_inline_command(path, arg, completion, **kwargs):
tlvdata.send(session.connection, {'operation': 'pause',
'path': currconsole})
buffdata = ''
while select.select((session.connection,), (), (), 0)[0]:
buffdata += consume_termdata(session.connection, bufferonly=True)
rc = session.simple_noderange_command(consolename, path, arg, **kwargs)
tlvdata.send(session.connection, {'operation': 'resume',
'path': currconsole})
sys.stdout.write(completion)
sys.stdout.flush()
if buffdata:
try:
sys.stdout.write(buffdata)
except UnicodeEncodeError:
sys.stdout.buffer.write(buffdata.encode('utf8'))
except IOError: # Some times circumstances are bad
# resort to byte at a time...
for d in buffdata:
sys.stdout.write(d)
return rc
def conserver_command(filehandle, localcommand):
# x - conserver has that as 'show baud', I am inclined to replace that with
# 'request exclusive'
@@ -740,13 +677,13 @@ def conserver_command(filehandle, localcommand):
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
if localcommand[1] == 'o': # off
sys.stdout.write("powering off...")
sys.stdout.flush()
run_inline_command('/power/state', 'off', 'complete]')
print("powering off...")
session.simple_noderange_command(consolename, '/power/state', 'off')
print("complete]\r")
elif localcommand[1] == 's': # shutdown
sys.stdout.write("shutting down...")
sys.stdout.flush()
run_inline_command('/power/state', 'shutdown', 'complete]')
print("shutting down...")
session.simple_noderange_command(consolename, '/power/state', 'shutdown')
print("complete]\r")
elif localcommand[1] == 'b': # boot
cmdlen += 1
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
@@ -757,46 +694,59 @@ def conserver_command(filehandle, localcommand):
bootmode = 'uefi'
bootdev = 'setup'
rc = run_inline_command('/boot/nextdevice', bootdev, '', bootmode=bootmode)
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = run_inline_command('/power/state', 'boot', 'complete]')
if rc:
print("Error]")
elif localcommand[2] == 'n': # boot to network
sys.stdout.write("booting to network...")
sys.stdout.flush()
bootmode = 'uefi'
bootdev = 'network'
rc = run_inline_command('/boot/nextdevice', bootdev, '', bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = run_inline_command('/power/state', 'boot', 'complete]')
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
elif localcommand[2] == 'n': # boot to network
print("booting to network...")
bootmode = 'uefi'
bootdev = 'network'
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
elif localcommand[2] == '\x0d': # boot to default
print("booting to default...")
bootmode = 'uefi'
bootdev = 'default'
rc = run_inline_command('/boot/nextdevice', bootdev, '', bootmode=bootmode)
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = run_inline_command('/power/state', 'boot', 'complete]')
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
else:
print("Unknown boot state.]\r")
else:
print("Unknown power state.]\r")
check_power_state()
elif localcommand[0] == 'r':
sys.stdout.write('\x1b7\x1b[999;999H\x1b[6n')
@@ -859,20 +809,6 @@ def check_escape_seq(currinput, filehandle):
currinput += filehandle.read()
return currinput
automation_directives = []
current_automation_directive = None
automation_map = {
'<up>': '\x1b[A',
'<down>': '\x1b[B',
'<right>': '\x1b[C',
'<left>': '\x1b[D',
'<enter>': '\r',
'<esc>': '\x1b',
'<tab>': '\t',
}
parser = optparse.OptionParser()
parser.add_option("-s", "--server", dest="netserver",
help="Confluent instance to connect to",
@@ -880,64 +816,12 @@ parser.add_option("-s", "--server", dest="netserver",
parser.add_option("-c", "--control", dest="controlpath",
help="Path to offer terminal control",
metavar="PATH")
parser.add_option('-a', '--automation', type='string', default=None,
help='Specify an automation script to run', metavar='SCRIPT')
parser.add_option('-e', '--headless', action='store_true', default=False,
help='Run in headless mode, which is designed for use with '
'automation scripts and disables interactive features')
parser.add_option(
'-m', '--mintime', default=0,
help='Minimum time to run or else pause for input (used to keep a '
'terminal from closing quickly on error)')
opts, shellargs = parser.parse_args()
def parse_automation_script(script, session_node):
global current_automation_directive
for line in script.splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('exit'):
automation_directives.append(('exit', None))
continue
if ' ' not in line:
sys.stderr.write("Invalid line in automation script: %s\n" % line)
continue
directive, arg = line.split(' ', 1)
directive = directive.strip().lower()
if directive not in ('expect', 'send', 'forget'):
sys.stderr.write("Unknown directive in automation script: %s\n" % directive)
continue
arg = arg.strip()
origarg = arg
if arg[0] not in ('"', "'"):
arg = '"' + arg + '"'
if arg[0] == "'" and arg[-1] == "'":
# do not process '<>' sequences in single quotes
arg = arg[1:-1]
arg = bytes(arg, "utf-8").decode("unicode_escape")
elif arg[0] == '"' and arg[-1] == '"':
arg = bytes(arg[1:-1], "utf-8").decode("unicode_escape")
for key, value in automation_map.items():
arg = arg.replace(key, value)
arg = re.sub(r'<env:(\w+)>', lambda m: os.environ[m.group(1)], arg)
if '{' in arg: # support confluent expressions
for res in session.create('/nodes/{0}/attributes/expression'.format(session_node),
{'expression': arg}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
sys.exit(1)
if 'value' in res:
arg = res['value']
automation_directives.append((directive, arg, origarg))
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
username = None
passphrase = None
def server_connect():
@@ -960,7 +844,7 @@ def server_connect():
passphrase = os.environ['CONFLUENT_PASSPHRASE']
session.authenticate(username, passphrase)
while not session.authenticated:
username = input("Name: ")
username = raw_input("Name: ")
passphrase = getpass.getpass("Passphrase: ")
session.authenticate(username, passphrase)
@@ -980,7 +864,7 @@ def main():
global inconsole
try:
server_connect()
except (EOFError, KeyboardInterrupt) as _:
except EOFError, KeyboardInterrupt:
raise BailOut(0)
except socket.gaierror:
sys.stderr.write('Could not connect to confluent\n')
@@ -990,7 +874,6 @@ def main():
# sys.stdout.write('\x1b[H\x1b[J')
# sys.stdout.flush()
global powerstate, powertime, clearpowermessage
if sys.stdout.isatty():
@@ -1004,16 +887,14 @@ def main():
doexit = False
inconsole = False
pendingcommand = ""
targ, session_node = get_session_node(shellargs)
session_node = get_session_node(shellargs)
if session_node is not None:
consoleonly = True
if opts.automation:
with open(opts.automation) as f:
parse_automation_script(f.read(), session_node)
do_command("start %s" % targ, netserver)
do_command("start /nodes/%s/console/session" % session_node, netserver)
doexit = True
elif shellargs:
do_command(shellargs, netserver)
command = " ".join(shellargs)
do_command(command, netserver)
quitconfetty(fullexit=True, fixterm=False)
powerstate = None
@@ -1021,18 +902,63 @@ def main():
while inconsole or not doexit:
if inconsole:
if opts.headless:
handles = [session.connection]
else:
handles = (sys.stdin, session.connection)
try:
rdylist, _, _ = select.select(
handles, (), (), 10)
(sys.stdin, session.connection), (), (), 10)
except select.error:
rdylist = ()
for fh in rdylist:
if fh == session.connection:
consume_termdata(fh)
# this only should get called in the
# case of a console session
# each command should slurp up all relevant
# recv potential
#fh.read()
try:
data = tlvdata.recv(fh)
except Exception:
data = None
if type(data) == dict:
updatestatus(data)
continue
if data is not None:
if clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
try:
sys.stdout.write(data)
except IOError: # Some times circumstances are bad
# resort to byte at a time...
for d in data:
sys.stdout.write(d)
now = time.time()
if ('showtime' not in laststate or
(now // 60) != laststate['showtime'] // 60):
# don't bother churning if minute does not change
laststate['showtime'] = now
updatestatus()
sys.stdout.flush()
else:
deadline = 5
connected = False
while not connected and deadline > 0:
try:
server_connect()
connected = True
except (socket.gaierror, socket.error):
pass
if not connected:
time.sleep(1)
deadline -=1
if connected:
do_command(
"start /nodes/%s/console/session skipreplay=True" % consolename,
netserver)
else:
doexit = True
inconsole = False
sys.stdout.write("\r\n[remote disconnected]\r\n")
break
else:
try:
myinput = fh.read()
@@ -1042,9 +968,8 @@ def main():
except IOError:
pass
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
if powerstate is None:
powerstate = True
powertime = time.time()
powerstate = True
check_power_state()
else:
currcommand = prompt()
@@ -1059,204 +984,6 @@ def main():
sys.stdout.write('Lost connection to server')
quitconfetty(fullexit=True)
sgr_re = re.compile(r'(\x1b\[[0-9;]*m)')
sgr_parameters_re = re.compile(r'\x1b\[([0-9;]*)m')
fgcolor = None
bgcolor = None
fgshifted = False
pendseq = ''
automation_check = ''
def check_automation(data):
global automation_check
global current_automation_directive
if type(data) != str:
data = data.decode('utf-8', errors='ignore')
while current_automation_directive:
if current_automation_directive[0] == 'forget' and data:
current_automation_directive = None
automation_check = ''
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\n')
sys.stdout.flush()
return
if current_automation_directive[0] == 'expect':
expected = current_automation_directive[1]
combined = automation_check + data
if expected and expected in combined:
data = data[combined.rindex(expected) + len(expected):]
if opts.headless:
sys.stdout.write(f'Detected {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
current_automation_directive = None
automation_check = ''
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
else:
# Check if there's potential start of expected data in the incoming data
combined = automation_check + data
automation_check = ''
for i in range(1, min(len(expected), len(combined)) + 1):
if expected.startswith(combined[-i:]):
automation_check = combined[-i:]
return # wait for next check
elif current_automation_directive[0] == 'send':
data = ''
automation_check = ''
if opts.headless:
sys.stdout.write(f'Sending {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
if current_automation_directive[1]:
tlvdata.send(session.connection, current_automation_directive[1])
current_automation_directive = None
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
elif current_automation_directive[0] == 'exit':
if opts.headless:
sys.stdout.write('Automation completed\r\n')
sys.stdout.flush()
return True
if opts.headless and not current_automation_directive:
sys.stdout.write('Automation completed\r\n')
sys.stdout.flush()
return True
return False
def consume_termdata(fh, bufferonly=False):
global clearpowermessage
global fgcolor, bgcolor, fgshifted, pendseq
try:
data = tlvdata.recv(fh)
except Exception:
data = None
if type(data) == dict:
updatestatus(data)
return ''
if data is not None:
shouldexit = check_automation(data)
if opts.headless:
if shouldexit:
quitconfetty(fullexit=True)
return ''
indata = pendseq + client.stringify(data)
pendseq = ''
data = ''
for segment in sgr_re.split(indata):
if sgr_re.match(segment): # it is an sgr, analyze, maybe replace
params = []
for parameters in sgr_parameters_re.findall(segment):
for param in parameters.split(';'):
params.append(param)
if param == '0':
fgcolor = None
bgcolor = None
try:
ival = int(param)
except ValueError:
continue
if 40 <= ival <= 47 or 100 <= ival <= 107:
bgcolor = ival
if 30 <= ival <= 37 or 90 <= ival <= 97:
fgcolor = ival
if bgcolor is not None:
fgindicated = False
for idx, param in enumerate(params):
try:
ival = int(param)
except ValueError:
continue
if 30 <= ival <= 37 and (bgcolor % 10 == ival % 10):
fgindicated = True
fgshifted = True
ival += 60
params[idx] = str(ival)
if not fgindicated and fgcolor is not None:
if bgcolor and (bgcolor % 10) == (fgcolor % 10):
fgshifted = True
params.append(str((fgcolor % 10) + 90))
elif fgshifted:
params.append(str(fgcolor))
segment = '\x1b[' + ';'.join(str(p) for p in params) + 'm'
data += segment
# defer any partial ansi escape sequence for a later pass
escidx = segment.rfind('\x1b[')
if escidx >= 0:
for chr in segment[escidx + 2:]:
if 0x40 <= ord(chr) <= 0x7e:
break
else:
# incomplete escape sequence, don't print it yet
data = data[:-len(segment) + escidx]
pendseq = segment[escidx:]
if not pendseq and segment and segment[-1] == '\x1b':
data = data[:-1]
pendseq = '\x1b'
if clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
if bufferonly:
return data
data = data.encode('utf8')
written = False
while not written:
select.select((), (sys.stdout,), ())
try:
sys.stdout.buffer.write(data)
written = True
except BlockingIOError:
continue
except IOError: # Some times circumstances are bad
# resort to byte at a time...
raise
for d in data:
sys.stdout.write(d)
written = True
now = time.time()
if ('showtime' not in laststate or
(now // 60) != laststate['showtime'] // 60):
# don't bother churning if minute does not change
laststate['showtime'] = now
updatestatus()
try:
sys.stdout.flush()
except Exception:
# EWOULDBLOCK causes this to raise, ignore
# this scenario comfortable that it
# will come out soon enough
pass
if shouldexit:
quitconfetty(fullexit=True)
else:
deadline = 5
connected = False
while not connected and deadline > 0:
try:
server_connect()
connected = True
except Exception:
pass
if not connected:
time.sleep(1)
deadline -=1
if connected:
do_command(
"start /nodes/%s/console/session skipreplay=True" % consolename,
netserver)
else:
doexit = True
inconsole = False
sys.stdout.write("\r\n[remote disconnected]\r\n")
return ''
if __name__ == '__main__':
errcode = 0
deadline = 0
@@ -1266,18 +993,8 @@ if __name__ == '__main__':
main()
except BailOut as e:
errcode = e.errorcode
except Exception as e:
import traceback
excinfo = traceback.print_exc()
try:
quitconfetty()
except Exception:
pass
print(excinfo)
finally:
if deadline and os.times()[4] < deadline:
sys.stderr.write('[Exited early, hit enter to continue]')
sys.stdin.readline()
if errcode == 0:
errcode = exitcode
sys.exit(errcode)
+25 -42
View File
@@ -1,9 +1,8 @@
#!/usr/bin/python3
#!/usr/bin/env python
import optparse
import signal
import sys
import os
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
@@ -18,16 +17,18 @@ import confluent.client as client
import confluent.sortutil as sortutil
def lookupdata(data, key):
ret = data.get(key, {}).get('value', '')
if ret is None:
ret = ''
return ret
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o ansible.hosts -a
''')
usage='''\n %prog noderange -o ansible.hosts
\n ''')
argparser.add_option('-o', '--output',
help='Writes an Ansible hosts file')
argparser.add_option('-a', '--append', action='store_true',
help='Appends to existing hosts file')
help='Ansible hosts file')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -44,42 +45,24 @@ def main():
if node not in databynode:
databynode[node] = {}
databynode[node].update(res['databynode'][node])
nodesbygroup = {}
for node in sortutil.natural_sort(databynode):
data = databynode[node]
groups = data.get('groups', [])
if not groups:
nodesbygroup.setdefault('', set()).add(node.strip().lower())
else:
for g in groups:
nodesbygroup.setdefault(g, set()).add(node.strip().lower())
existing_data = {}
if options.append and os.path.exists(options.output):
current_group = ''
with open(options.output, 'r') as f:
for line in f:
line = line.strip().lower()
if not line:
continue
if line.startswith('[') and line.endswith(']'):
current_group = line[1:-1]
existing_data.setdefault(current_group, set())
else:
existing_data.setdefault(current_group, set()).add(line)
for group, nodes in nodesbygroup.items():
nodes = {n.strip().lower() for n in nodes}
current_nodes = existing_data.get(group, set())
new_nodes = nodes - current_nodes
if new_nodes:
existing_data.setdefault(group, set()).update(nodes)
with open(options.output, 'w') as importfile:
for group in sortutil.natural_sort(existing_data.keys()):
if group:
importfile.write('[{0}]\n'.format(group))
for node in sortutil.natural_sort(existing_data[group]):
needempty = False
for node in sortutil.natural_sort(databynode):
data = databynode[node]
if not data.get('groups', []):
importfile.write(node + '\n')
needempty = True
for g in data.get('groups', []):
if g not in nodesbygroup:
nodesbygroup[g] = set([node])
else:
nodesbygroup[g].add(node)
if needempty:
importfile.write('\n')
for group in sortutil.natural_sort(nodesbygroup):
importfile.write('[{0}]\n'.format(group))
for node in sortutil.natural_sort(nodesbygroup[group]):
importfile.write('{0}\n'.format(node))
importfile.write('\n')
-567
View File
@@ -1,567 +0,0 @@
#!/usr/bin/python3
# Generate a dnsmasq static DHCP configuration (a file under /etc/dnsmasq.d)
# from the confluent attribute database. One "dhcp-host=" reservation is
# emitted for every network of every node that has a hardware (MAC) address
# defined (net.hwaddr, net.<iface>.hwaddr, net.bmc.hwaddr, ...).
import argparse
import ipaddress
import json
import os
import re
import signal
import subprocess
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
TARGET = '/etc/dnsmasq.d/confluent-dhcp.conf'
TAGPREFIX = 'cdhcp'
# Invocation flags that do not affect the generated content; excluded from the
# recorded "Regenerate:" line and from the settings-changed comparison.
NONCONFIG_ARGS = {'-y', '--yes', '-n', '--stdout'}
def split_list(val):
"""Split a confluent comma/whitespace delimited attribute value."""
if not val:
return []
return [v for v in re.split(r'[\s,]+', val.strip()) if v]
def config_args(argv):
"""Join the invocation arguments that affect the generated content."""
return ' '.join(a for a in argv if a not in NONCONFIG_ARGS)
def parse_net_attrib(attrib):
"""Split a net.* attribute into (network_name, field).
The field is always the final dotted component; everything between
'net.' and the field is the network name, which may itself span
several dotted components.
"""
parts = attrib.split('.')
field = parts[-1]
if len(parts) == 2:
currnet = None
else:
currnet = '.'.join(parts[1:-1])
return currnet, field
def subnet_sort_key(netaddr):
try:
return tuple(int(o) for o in netaddr.split('.'))
except ValueError:
return (0, 0, 0, 0)
def parse_routes():
"""Parse `ip --json route` into a list of (network, route_dict) tuples for
routes with a concrete destination prefix (default routes and the like are
skipped). Returns [] if iproute2 or its JSON output is unavailable."""
try:
out = subprocess.check_output(['ip', '--json', 'route'],
stderr=subprocess.DEVNULL)
except (OSError, subprocess.SubprocessError):
return []
try:
entries = json.loads(out.decode('utf-8', 'replace'))
except ValueError:
return []
routes = []
for route in entries:
dst = route.get('dst')
if not dst or dst == 'default':
continue
if '/' not in dst:
dst += '/32'
try:
net = ipaddress.ip_network(dst, strict=False)
except ValueError:
continue
routes.append((net, route))
return routes
def connected_network_for_ip(routes, ipstr):
"""Most specific directly-attached (prefsrc-bearing) route network that
contains `ipstr`, or None. Used to fill in a missing prefix length."""
try:
ip = ipaddress.ip_address(ipstr)
except ValueError:
return None
best = None
for net, route in routes:
if 'prefsrc' not in route:
continue
if ip in net and (best is None or net.prefixlen > best.prefixlen):
best = net
return best
def prefsrc_for_network(routes, network):
"""This host's preferred source address on `network` per the routing table
(the address dnsmasq should listen on for that subnet), or None."""
fallback = None
for net, route in routes:
prefsrc = route.get('prefsrc')
if not prefsrc:
continue
if net == network:
return prefsrc
if network.network_address in net:
fallback = prefsrc
return fallback
def existing_regen_args(targetpath):
"""Return the content arguments recorded in an existing config's
'Regenerate:' header line, or None if not present/readable."""
try:
with open(targetpath) as cfg:
for line in cfg:
if line.startswith('# Regenerate:'):
rest = line[len('# Regenerate:'):].strip()
marker = 'confluent2dnsmasq'
if rest.startswith(marker):
return rest[len(marker):].strip()
return rest
if not line.startswith('#'):
break
except OSError:
pass
return None
def warn_conflicts(hostentries):
"""Warn about reservations that conflict with one another: the same
hostname on different IPs, one IP reserved more than once (dnsmasq
refuses to start on a duplicate dhcp-host IP), or one MAC reserved
more than once. Diagnostics only; every entry is still emitted."""
byname = {}
byip = {}
bymac = {}
for e in hostentries:
if e['hostname']:
prev = byname.setdefault(e['hostname'], e)
if prev is not e and prev['ip'] != e['ip']:
sys.stderr.write(
"WARNING: hostname '{0}' maps to both {1} ({2}) and "
'{3} ({4})\n'.format(
e['hostname'], prev['ip'], prev['node'],
e['ip'], e['node']))
prev = byip.setdefault(e['ip'], e)
if prev is not e:
sys.stderr.write(
'WARNING: address {0} is reserved for both {1} ({2}) and '
'{3} ({4}); dnsmasq refuses to start on a duplicate '
'dhcp-host IP address\n'.format(
e['ip'], ','.join(prev['macs']), prev['node'],
','.join(e['macs']), e['node']))
for mac in e['macs']:
prev = bymac.setdefault(mac.lower(), e)
if prev is not e:
sys.stderr.write(
'WARNING: MAC {0} is reserved for both {1} ({2}) and '
'{3} ({4})\n'.format(
mac, prev['ip'], prev['node'],
e['ip'], e['node']))
def main():
ap = argparse.ArgumentParser(
description='Create /etc/dnsmasq.d static DHCP reservations for a '
'noderange from the confluent database')
ap.add_argument('noderange',
help='Noderange to generate DHCP reservations for')
ap.add_argument('--target', default=TARGET, metavar='PATH',
help='Output configuration file (default: %(default)s).')
ap.add_argument('--tag-prefix', default=TAGPREFIX, metavar='PREFIX',
help='Prefix for generated dnsmasq tag names '
'(default: %(default)s).')
ap.add_argument('--no-bind-dynamic', dest='emit_bind_dynamic',
action='store_false',
help="Do not emit the 'bind-dynamic' line "
'(it is emitted by default).')
ap.add_argument('--no-listen-address', dest='detect_listen',
action='store_false',
help='Do not autodetect and emit a listen-address line for '
"this host's address on each managed subnet. By "
'default they are emitted together with localhost '
'(127.0.0.1 and ::1).')
ap.add_argument('--no-range', dest='emit_range', action='store_false',
help='Do not emit dhcp-range lines (manage ranges '
'elsewhere). Note: dnsmasq will not answer DHCP on a '
'subnet that has no covering dhcp-range.')
ap.add_argument('--lease', default='24h', metavar='TIME',
help='Lease time for emitted dhcp-range lines '
'(default: 24h; e.g. 1h, 24h, infinite). '
'Pass an empty string to omit it.')
ap.add_argument('-n', '--stdout', action='store_true',
help='Write the configuration to stdout instead of '
'writing the target file.')
ap.add_argument('-y', '--yes', dest='assume_yes', action='store_true',
help='Do not prompt for confirmation when an existing '
'config was generated with different settings.')
ap.add_argument('--allow-empty', action='store_true',
help='Write the file even if no reservations were '
'produced (default: refuse, to avoid clobbering an '
'existing config on an empty/failed read).')
# Optional extra data injected into the DHCP reply, sourced from the
# confluent database. All are OFF by default and scoped per-subnet.
grp = ap.add_argument_group(
'optional DHCP reply data (sourced from confluent, off by default)')
grp.add_argument('--all-options', action='store_true',
help='Enable every optional reply datum listed below.')
grp.add_argument('--gateway', '--router', dest='opt_gateway',
action='store_true',
help='router, option 3, from net.<net>.ipv4_gateway')
grp.add_argument('--dns', dest='opt_dns', action='store_true',
help='dns-server, option 6, from dns.servers')
grp.add_argument('--domain', dest='opt_domain', action='store_true',
help='domain-name, option 15, from dns.domain')
grp.add_argument('--ntp', dest='opt_ntp', action='store_true',
help='ntp-server, option 42, from ntp.servers')
grp.add_argument('--mtu', dest='opt_mtu', action='store_true',
help='interface-mtu, option 26, from net.<net>.mtu')
args = ap.parse_args()
if args.all_options:
args.opt_gateway = True
args.opt_dns = True
args.opt_domain = True
args.opt_ntp = True
args.opt_mtu = True
c = client.Command()
# Kernel routing table (ip --json route), used to fill in a missing prefix
# length on a node address and to pick this host's listen-address (prefsrc)
# for each managed subnet.
routes = parse_routes()
# Node level attributes
domainbynode = {}
dnsbynode = {}
ntpbynode = {}
# Per (node, net) network attributes: nets[node][net] = {field: value}
nets = {}
read_error = False
for ent in c.read('/noderange/{0}/attributes/current'.format(
args.noderange)):
if 'error' in ent:
sys.stderr.write(ent['error'] + '\n')
read_error = True
continue
ent = ent.get('databynode', {})
for node in ent:
for attrib in ent[node]:
val = ent[node][attrib]
if not isinstance(val, dict):
continue
val = val.get('value', None)
if val in (None, ''):
continue
if attrib == 'dns.domain':
domainbynode[node] = val
elif attrib == 'dns.servers':
dnsbynode[node] = val
elif attrib == 'ntp.servers':
ntpbynode[node] = val
elif attrib.startswith('net.'):
currnet, field = parse_net_attrib(attrib)
if field not in ('hwaddr', 'ipv4_address',
'ipv4_gateway', 'hostname', 'mtu'):
continue
nets.setdefault(node, {}).setdefault(currnet, {})
nets[node][currnet][field] = val
# Build reservation entries and aggregate per-subnet information.
hostentries = [] # {subnetkey, macs, ip, hostname}
subnets = {} # subnetkey (netaddr, prefixlen) -> aggregate dict
for node in sorted(nets):
netnames = sorted(nets[node], key=lambda n: (n is not None, n or ''))
for currnet in netnames:
slot = nets[node][currnet]
hwaddr = slot.get('hwaddr')
if not hwaddr:
continue
label = currnet if currnet else '(primary)'
netprefix = (currnet + '.') if currnet else ''
addr = slot.get('ipv4_address')
if not addr:
sys.stderr.write(
'{0}: net {1} has a hwaddr but no net.{2}ipv4_address; '
'skipping\n'.format(node, label, netprefix))
continue
# If confluent has no prefix length on the address, borrow it from
# the matching directly-attached route.
if '/' not in addr:
cnet = connected_network_for_ip(routes, addr)
if cnet is not None:
addr = '{0}/{1}'.format(addr, cnet.prefixlen)
ip = addr.split('/', 1)[0]
# Reservation hostname: first token of the per-net hostname,
# else the node name for the primary (unnamed) network, else
# omit it (named net with no hostname).
hostname = None
if slot.get('hostname'):
hostname = split_list(slot['hostname'])[0]
elif currnet is None:
hostname = node
# Derive the subnet (needed for dhcp-range and for scoping
# the optional dhcp-option tags).
subnetkey = None
if '/' in addr:
try:
iface = ipaddress.ip_interface(addr)
except ValueError as e:
sys.stderr.write(
'{0}: net {1} has invalid address {2} ({3}); '
'skipping\n'.format(node, label, addr, e))
continue
network = iface.network
subnetkey = (str(network.network_address), network.prefixlen)
sub = subnets.get(subnetkey)
if sub is None:
tag = '{0}_{1}_{2}'.format(
args.tag_prefix,
str(network.network_address).replace('.', '_'),
network.prefixlen)
sub = {'netmask': str(network.netmask), 'tag': tag,
'prefsrc': prefsrc_for_network(routes, network),
'gateway': set(), 'dns': set(), 'domain': set(),
'ntp': set(), 'mtu': set()}
subnets[subnetkey] = sub
if slot.get('ipv4_gateway'):
sub['gateway'].add(slot['ipv4_gateway'])
if node in dnsbynode:
sub['dns'].add(dnsbynode[node])
if node in domainbynode:
sub['domain'].add(domainbynode[node])
if node in ntpbynode:
sub['ntp'].add(ntpbynode[node])
if slot.get('mtu'):
sub['mtu'].add(slot['mtu'])
elif args.emit_range:
sys.stderr.write(
'{0}: net {1} address {2} has no /prefix; cannot derive a '
'subnet for dhcp-range (add a prefix or use --no-range). '
'Emitting the dhcp-host anyway.\n'.format(
node, label, addr))
hostentries.append({'subnetkey': subnetkey,
'macs': split_list(hwaddr),
'ip': ip, 'hostname': hostname,
'node': node})
warn_conflicts(hostentries)
# Resolve optional per-subnet options from the aggregated values.
def resolve(values, what, subnetkey):
if not values:
return None
if len(values) > 1:
chosen = sorted(values)[0]
sys.stderr.write(
'subnet {0}/{1}: inconsistent {2} across nodes {3}; using '
'{4}\n'.format(subnetkey[0], subnetkey[1], what,
sorted(values), chosen))
return chosen
return next(iter(values))
optionlines = {} # subnetkey -> [ 'option:...,value', ... ]
for subnetkey, sub in subnets.items():
lines = []
if args.opt_gateway:
gw = resolve(sub['gateway'], 'net.ipv4_gateway', subnetkey)
if gw:
lines.append('option:router,{0}'.format(gw))
if args.opt_dns:
dv = resolve(sub['dns'], 'dns.servers', subnetkey)
if dv:
lines.append('option:dns-server,{0}'.format(
','.join(split_list(dv))))
if args.opt_domain:
dom = resolve(sub['domain'], 'dns.domain', subnetkey)
if dom:
lines.append('option:domain-name,{0}'.format(dom))
if args.opt_ntp:
nv = resolve(sub['ntp'], 'ntp.servers', subnetkey)
if nv:
lines.append('option:ntp-server,{0}'.format(
','.join(split_list(nv))))
if args.opt_mtu:
mv = resolve(sub['mtu'], 'net.mtu', subnetkey)
if mv:
lines.append('option:mtu,{0}'.format(mv))
if lines:
optionlines[subnetkey] = lines
orderedsubnets = sorted(subnets,
key=lambda k: subnet_sort_key(k[0]) + (k[1],))
# Render.
out = ['# Managed by confluent2dnsmasq -- DO NOT EDIT BY HAND.',
'# Regenerate: confluent2dnsmasq {0}'.format(
config_args(sys.argv[1:])),
'# Generated {0}'.format(time.strftime('%Y-%m-%d %H:%M:%S %z')),
'']
if args.emit_bind_dynamic:
out.append('# Allow confluent and dnsmasq to share the same network '
'for DHCP')
out.append('bind-dynamic')
out.append('')
detected = []
if args.detect_listen:
for subnetkey in orderedsubnets:
prefsrc = subnets[subnetkey].get('prefsrc')
if prefsrc and prefsrc not in detected:
detected.append(prefsrc)
out.append('# Listen on localhost plus this host\'s address on each '
'managed subnet,')
out.append('# so dnsmasq serves these networks alongside confluent '
'and bind-dynamic')
out.append('# (a listen-address line also overrides local-service in '
'dnsmasq.conf).')
out.append('listen-address=127.0.0.1')
out.append('listen-address=::1')
for addr in detected:
out.append('listen-address={0}'.format(addr))
out.append('')
hosts_by_subnet = {}
nosubnet_hosts = []
for e in hostentries:
fields = list(e['macs'])
if e['subnetkey'] in optionlines:
fields.append('set:{0}'.format(subnets[e['subnetkey']]['tag']))
fields.append(e['ip'])
if e['hostname']:
fields.append(e['hostname'])
line = 'dhcp-host={0}'.format(','.join(fields))
if e['subnetkey'] is None:
nosubnet_hosts.append(line)
else:
hosts_by_subnet.setdefault(e['subnetkey'], []).append(line)
for subnetkey in orderedsubnets:
sub = subnets[subnetkey]
netaddr, prefixlen = subnetkey
out.append('# subnet {0}/{1}'.format(netaddr, prefixlen))
if args.emit_range:
rangefields = [netaddr, 'static', sub['netmask']]
if args.lease:
rangefields.append(args.lease)
out.append('dhcp-range={0}'.format(','.join(rangefields)))
for optline in optionlines.get(subnetkey, []):
out.append('dhcp-option=tag:{0},{1}'.format(sub['tag'], optline))
for line in sorted(hosts_by_subnet.get(subnetkey, [])):
out.append(line)
out.append('')
if nosubnet_hosts:
out.append('# reservations with no derivable subnet (address had no '
'/prefix)')
out.extend(sorted(nosubnet_hosts))
out.append('')
text = '\n'.join(out).rstrip('\n') + '\n'
# Warnings about configuration choices that affect coexistence with
# confluent's own DHCP service.
if args.detect_listen and subnets and not detected:
sys.stderr.write(
'WARNING: no local address was found on any managed subnet, so '
'dnsmasq will only listen on localhost. Run this on the dnsmasq '
'host, or set interface/listen-address manually.\n')
if not args.detect_listen:
sys.stderr.write(
'WARNING: listen-address autodetection disabled. For dnsmasq to '
'serve these networks together with confluent and bind-dynamic, '
'set interface, except-interface or listen-address manually, or '
'disable local-service=host in dnsmasq.conf.\n')
if not args.emit_bind_dynamic:
sys.stderr.write(
'WARNING: bind-dynamic not emitted; this can conflict with '
"confluent's DHCP unless bind-dynamic is set elsewhere in the "
'dnsmasq configuration.\n')
if args.stdout:
sys.stdout.write(text)
return
if not hostentries and not args.allow_empty:
sys.stderr.write(
'No DHCP reservations were generated (no networks with both a '
'hwaddr and an ipv4_address were found){0}. Refusing to overwrite '
'{1}; pass --allow-empty to override.\n'.format(
' and there were read errors' if read_error else '',
args.target))
sys.exit(1)
# If a config already exists that was generated with different
# content-affecting settings, confirm before overwriting it.
if os.path.exists(args.target):
prev = existing_regen_args(args.target)
cur = config_args(sys.argv[1:])
if prev is not None and prev != cur:
sys.stderr.write(
'Existing {0} was generated with different settings:\n'
' old: confluent2dnsmasq {1}\n'
' new: confluent2dnsmasq {2}\n'.format(
args.target, prev or '(no arguments)',
cur or '(no arguments)'))
if not args.assume_yes:
if not sys.stdin.isatty():
sys.stderr.write(
'Refusing to regenerate with changed settings '
'non-interactively; pass -y/--yes to confirm.\n')
sys.exit(1)
try:
resp = input('Are you sure you want to regenerate using the new settings? [y/N] ')
except EOFError:
resp = ''
if resp.strip().lower() not in ('y', 'yes'):
sys.stderr.write(
'Aborted; {0} left unchanged.\n'.format(args.target))
sys.exit(1)
targetdir = os.path.dirname(args.target)
targetname = os.path.basename(args.target)
if targetdir and not os.path.isdir(targetdir):
os.makedirs(targetdir, exist_ok=True)
# Write via a hidden temp file in the same directory, then rename: the
# rename is atomic, and the dot-prefix keeps a dnsmasq conf-dir from
# loading the temp file if it scans mid-write.
tmp = os.path.join(targetdir, '.' + targetname + '.tmp')
with open(tmp, 'w') as cfg:
cfg.write(text)
os.replace(tmp, args.target)
sys.stderr.write('Wrote {0} reservation(s) across {1} subnet(s) to {2}\n'
.format(len(hostentries), len(subnets), args.target))
sys.stderr.write('Be sure to restart dnsmasq for the changes to take effect\n')
if __name__ == '__main__':
main()
-227
View File
@@ -1,227 +0,0 @@
#!/usr/bin/python2
import argparse
import os
import re
import signal
import shutil
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
def partitionhostsline(line):
comment = ''
if '#' in line:
cmdidx = line.index('#')
comment = line[cmdidx:]
line = line[:cmdidx].strip()
if not line.strip():
return '', [], comment
ipaddr, names = line.split(None, 1)
names = names.split()
return ipaddr, names, comment
class HostMerger(object):
def __init__(self):
self.byip = {}
self.byname = {}
self.byname6 = {}
self.ipbyname = {}
self.ipbyname6 = {}
self.sourcelines = []
self.targlines = []
def read_source(self, sourcefile):
with open(sourcefile, 'r') as hfile:
self.sourcelines = hfile.read().split('\n')
while not self.sourcelines[-1]:
self.sourcelines = self.sourcelines[:-1]
for x in range(len(self.sourcelines)):
line = self.sourcelines[x]
currip, names, comment = partitionhostsline(line)
if currip:
self.byip[currip] = x
byname = self.byname
if ':' in currip:
byname = self.byname6
for name in names:
byname[name] = x
def add_entry(self, ip, names):
targ = self.byname
ipbyname = self.ipbyname
if ':' in ip:
targ = self.byname6
ipbyname = self.ipbyname6
line = '{:<39} {}'.format(ip, names)
x = len(self.sourcelines)
self.sourcelines.append(line)
for name in names.split():
if not name:
continue
if ipbyname.setdefault(name, ip) != ip:
sys.stderr.write(
'WARNING: {0} is mapped to both {1} and {2}; all entries '
'will be written to /etc/hosts\n'.format(
name, ipbyname[name], ip))
targ[name] = x
self.byip[ip] = x
def read_target(self, targetfile):
if not os.path.exists(targetfile):
return
with open(targetfile, 'r') as hfile:
lines = hfile.read().split('\n')
while lines and not lines[-1]:
lines = lines[:-1]
for y in range(len(lines)):
line = lines[y]
currip, names, comment = partitionhostsline(line)
byname = self.byname
if ':' in currip:
byname = self.byname6
if currip in self.byip:
x = self.byip[currip]
if self.sourcelines[x] is None:
# have already consumed this entry
continue
self.targlines.append(self.sourcelines[x])
self.sourcelines[x] = None
continue
for name in names:
if name in byname:
x = byname[name]
if self.sourcelines[x] is None:
break
self.targlines.append(self.sourcelines[x])
self.sourcelines[x] = None
break
else:
self.targlines.append(line)
def write_out(self, targetfile):
while self.targlines and not self.targlines[-1]:
self.targlines = self.targlines[:-1]
while self.sourcelines and not self.sourcelines[-1]:
self.sourcelines = self.sourcelines[:-1]
if not self.sourcelines:
break
with open(targetfile, 'w') as hosts:
for line in self.targlines:
hosts.write(line + '\n')
for line in self.sourcelines:
if line is not None:
hosts.write(line + '\n')
def main():
ap = argparse.ArgumentParser(description="Create/amend /etc/hosts file for given noderange")
ap.add_argument('noderange', help='Noderange to generate/update /etc/hosts for')
ap.add_argument('-a', '--attrib', help='Pull ip addresses and hostnames from attribute database', action='store_true')
ap.add_argument('-i', '--ip', help='Expression to generate addresses (e.g. 172.16.1.{n1} or fd2b:246f:8a50::{n1:x})')
ap.add_argument('-n', '--name', help='Expression for name to add ({node}-compute, etc). If unspecified, "{node} {node}.{dns.domain}" will be used', action='append')
ap.add_argument('-f', '--fqdn-first', help='Put the FQDN first in the hosts entries', action='store_true')
args = ap.parse_args()
c = client.Command()
if args.name:
names = ' '.join(args.name)
else:
names = '{node} {node}.{dns.domain}'
merger = HostMerger()
if not args.ip and not args.attrib:
sys.stderr.write('-a or -i is currently required\n')
sys.exit(1)
if args.attrib:
ip4bynode = {}
ip6bynode = {}
namesbynode = {}
domainsbynode = {}
for ent in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
ent = ent.get('databynode', {})
for node in ent:
for attrib in ent[node]:
val = ent[node][attrib]
if not isinstance(val, dict):
continue
val = val.get('value', None)
if attrib == 'dns.domain':
domainsbynode[node] = val
if attrib.startswith('net.'):
nameparts = attrib.split('.')
if len(nameparts) == 2:
currnet = None
else:
currnet = '.'.join(nameparts[1:-1])
for currdict in (ip4bynode, ip6bynode, namesbynode):
if node not in currdict:
currdict[node] = {}
if attrib.endswith('.ipv4_address') and val:
ip4bynode[node][currnet] = val.split('/', 1)[0]
elif attrib.endswith('.ipv6_address') and val:
ip6bynode[node][currnet] = val.split('/', 1)[0]
elif attrib.endswith('.hostname'):
namesbynode[node][currnet] = re.split(r'\s+|,', val)
for node in ip4bynode:
mydomain = domainsbynode.get(node, None)
for ipdb in (ip4bynode, ip6bynode):
for currnet in ipdb[node]:
if args.name:
sys.stderr.write('Custom name and attribute addresses not currently supported together\n')
sys.exit(1)
else:
names = list(namesbynode.get(node, {}).get(currnet, [node]))
if mydomain:
for name in names:
if mydomain in names:
break
else:
for name in list(names):
fqdn = '{0}.{1}'.format(name, mydomain)
if args.fqdn_first:
# Insert FQDN at the beginning if --fqdn-first flag is set
names.insert(0, fqdn)
else:
# Otherwise, append FQDN at the end (original behavior)
names.append(fqdn)
names = ' '.join(names)
merger.add_entry(ipdb[node][currnet], names)
else:
namesbynode = {}
ipbynode = {}
expurl = '/noderange/{0}/attributes/expression'.format(args.noderange)
expression = names
exitcode = 0
exitcode |= expand_expression(c, namesbynode, expurl, names)
exitcode |= expand_expression(c, ipbynode, expurl, args.ip)
if exitcode:
sys.exit(exitcode)
for node in ipbynode:
merger.add_entry(ipbynode[node], namesbynode[node])
if os.path.exists('/etc/hosts'):
merger.read_target('/etc/hosts')
shutil.copy2('/etc/hosts', '/etc/hosts.confluentbkup')
merger.write_out('/etc/hosts')
def expand_expression(c, namesbynode, expurl, expression):
exitcode = 0
for exp in c.create(expurl, {'expression': expression}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
namesbynode[node] = ex[node]['value']
return exitcode
if __name__ == '__main__':
main()
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
import csv
import optparse
import signal
+2 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
import csv
import optparse
import signal
@@ -26,7 +26,7 @@ def lookupdata(data, key):
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o xcatnodes.def
usage='''\n %prog noderange -o ansible.hosts
\n ''')
argparser.add_option('-o', '--output',
help='xCAT stanza file')
@@ -79,8 +79,6 @@ def collect_attribute_data(databynode, node):
xcatattrs['groups'] = ','.join(data.get('groups', []))
xcatattrs['bmc'] = data.get('hardwaremanagement.manager', {}).get(
'value', None)
if xcatattrs['bmc']:
xcatattrs['bmc'] = xcatattrs['bmc'].split('/', 1)[0]
xcatattrs['mpa'] = data.get('enclosure.manager', {}).get(
'value', None)
xcatattrs['slotid'] = data.get('enclosure.bay', {}).get(
+16 -78
View File
@@ -1,81 +1,19 @@
#!/usr/bin/python
#!/bin/sh
# This will take a given directory and make a 'big floppy image'
# out of it, suitable for nodemedia upload.
import fcntl
import glob
import os
import subprocess
import sys
def create_image(directory, image, label=None, esize=0, totalsize=None):
if totalsize:
datasz = totalsize * 1048576
else:
ents = 0
datasz = 512 + (esize * 1048576)
for dir in os.walk(sys.argv[1]):
ents += 1
for filen in dir[2]:
ents += 1
filename = os.path.join(dir[0], filen)
currsz = os.path.getsize(filename)
# assuming up to 65k cluster
currsz = (currsz // 512 + 1) * 512
datasz += currsz
datasz += ents * 32768
datasz = datasz // 65536 + 1
with open(image, 'wb') as imgfile:
imgfile.seek(datasz * 65536 - 1)
imgfile.write(b'\x00')
if label:
# 4 heads, 32 sectors, means 65k per track
subprocess.check_call(['mformat', '-i', image, '-v', label,
'-r', '16', '-d', '1', '-t', str(datasz),
'-s', '32','-h', '4', '::'])
else:
subprocess.check_call(['mformat', '-i', image, '-r', '16', '-d', '1', '-t',
str(datasz), '-s', '32','-h', '4', '::'])
# Some clustered filesystems will have the lock from mformat
# linger after close (mformat doesn't unlock)
# do a blocking wait for shared lock and then explicitly
# unlock between calls to mtools
with open(image, 'rb') as imgfile:
fcntl.flock(imgfile.fileno(), fcntl.LOCK_SH)
fcntl.flock(imgfile.fileno(), fcntl.LOCK_UN)
cpycmd = ['mcopy', '-i', image, '-s']
cpycmd.extend(glob.glob('{0}/*'.format(directory)))
cpycmd.append('::')
subprocess.check_call(cpycmd)
# though not necessary for us, make sure dir2img doesn't have a lingering
# flock from mcopy for any subsequent commands
with open(image, 'rb') as imgfile:
fcntl.flock(imgfile.fileno(), fcntl.LOCK_SH)
fcntl.flock(imgfile.fileno(), fcntl.LOCK_UN)
if __name__ == '__main__':
if len(sys.argv) < 3:
sys.stderr.write("Usage: {0} <directory> <imagefile>\n".format(
sys.argv[0]))
sys.exit(1)
label = None
args = sys.argv
esize = 0
try:
earg = args.index('-e')
esize = int(args[earg + 1])
args = args[:earg] + args[earg +2:]
except ValueError:
pass
totsize = None
try:
earg = args.index('-s')
totsize = int(args[earg + 1])
args = args[:earg] + args[earg +2:]
except ValueError:
pass
if len(args) > 3:
label = args[3]
create_image(args[1], args[2], label, esize, totsize)
if [ -z "$1" -o -z "$2" ]; then
echo "Usage: $0 <directory> <imagefile>"
exit 1
fi
# Get the needed payload side
SIZE=$(du -sB 512 $1|awk '{print $1}')
ENTRIES=$(find $1 |wc -l)
# Also, each file and directory has overhead, pad size by 32kb per file,
# which should be overkill but other values proved to be inadequate
# A deeper understanding of VFAT would probably allow for more precise value
SIZE=$((SIZE + ENTRIES * 64))
dd if=/dev/zero of=$2 bs=512 count=$SIZE
# Make a big single sided floppy with many many tracks, saves on complex math
mformat -i $2 -d 1 -t $SIZE -s 1 -h 1 ::
mcopy -i $2 -s $1/* ::
-218
View File
@@ -1,218 +0,0 @@
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# 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.
from collections import deque
import optparse
import os
import select
import signal
import subprocess
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
devnull = None
def run_automation(noderange, category, c):
exitcode = 0
automationbynode = {}
for res in c.update('/noderange/{0}/deployment/remote_config/run'.format(noderange), {
'category': category,
}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
if 'created' in res:
nodename = res['created'].split('/')[2]
automationbynode[nodename] = res['created']
while automationbynode:
for node in list(automationbynode):
for res in c.read(automationbynode[node]):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
for result in res.get('results', []):
sys.stdout.write('{0}: Task [{1}] {2}\n'.format(
node, result['task_name'], result['state']))
for warning in result.get('warnings', []):
sys.stderr.write('{0}: [WARNING] {1}\n'.format(node, warning))
if 'errorinfo' in result:
for errorline in result['errorinfo'].splitlines():
sys.stderr.write('{0}: [ERROR] {1}\n'.format(node, errorline))
if res.get('complete', False):
del automationbynode[node]
sys.stdout.write('{0}: Automation complete\n'.format(node))
return exitcode
def run():
global devnull
devnull = open(os.devnull, 'rb')
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
argparser.add_option('-k', '--security', action='store_true',
help='Update SSH setup')
argparser.add_option('-F', '--sync', action='store_true',
help='Run the syncfiles associated with the currently completed OS profile on the noderange')
argparser.add_option('-P', '--scripts',
help='Re-run specified scripts, with full path under scripts, e.g. post.d/first,firstboot.d/second')
argparser.add_option('-A', '--automation',
help='Run the automation scripts associated with the current OS profile on the noderange, specifying category (onboot.d/firstboot.d/post.d)')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run remote ssh command to, '
'prompting if over the threshold')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
#argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 1:
argparser.print_help()
sys.exit(1)
client.check_globbing(args[0])
concurrentprocs = options.count
c = client.Command()
currprocs = 0
all = set([])
poller = select.epoll()
pipedesc = {}
pendingexecs = deque()
exitcode = 0
# Kept apart from exitcode: a failed automation run must not trip the
# early exit below, which would abandon ssh children already spawned.
autoexitcode = 0
c.stop_if_noderange_over(args[0], options.maxnodes)
if options.automation:
autoexitcode = run_automation(args[0], options.automation, c)
nodemap = {}
cmdparms = []
nodes = []
cmdstorun = []
if options.security:
cmdstorun.append(['run_remote', 'setupssh'])
if options.sync:
cmdstorun.append(['run_remote_python', 'syncfileclient'])
if options.scripts:
for script in options.scripts.split(','):
cmdstorun.append(['run_remote', script])
if not cmdstorun:
if options.automation:
sys.exit(autoexitcode)
argparser.print_help()
sys.exit(1)
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
break
node = res['item']['href'][:-1]
nodes.append(node)
idxbynode = {}
cmdvbase = ['bash', '/etc/confluent/functions']
for sshnode in nodes:
idxbynode[sshnode] = 1
cmdv = ['ssh', sshnode] + cmdvbase + cmdstorun[0]
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(sshnode, cmdv, all, poller, pipedesc)
else:
pendingexecs.append((sshnode, cmdv))
if not all or exitcode:
sys.exit(exitcode | autoexitcode)
rdy = poller.poll(10)
while all:
pernodeout = {}
for r in rdy:
r = r[0]
desc = pipedesc[r]
r = desc['file']
node = desc['node']
data = True
singlepoller = select.epoll()
singlepoller.register(r, select.EPOLLIN)
while data and singlepoller.poll(0):
data = r.readline()
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = []
pernodeout[node].append(data)
else:
data = client.stringify(data)
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
poller.unregister(r)
r.close()
if desc['type'] == 'stdout':
if idxbynode[node] < len(cmdstorun):
cmdv = ['ssh', sshnode] + cmdvbase + cmdstorun[idxbynode[node]]
idxbynode[node] += 1
run_cmdv(node, cmdv, all, poller, pipedesc)
elif pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, poller, pipedesc)
singlepoller.close()
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
line = line.lstrip('\x08')
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy = poller.poll(10)
sys.exit(exitcode | autoexitcode)
def run_cmdv(node, cmdv, all, poller, pipedesc):
nopen = subprocess.Popen(
cmdv, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pipedesc[nopen.stdout.fileno()] = {'node': node, 'popen': nopen,
'type': 'stdout', 'file': nopen.stdout}
pipedesc[nopen.stderr.fileno()] = {'node': node, 'popen': nopen,
'type': 'stderr', 'file': nopen.stderr}
all.add(nopen.stdout)
poller.register(nopen.stdout, select.EPOLLIN)
all.add(nopen.stderr)
poller.register(nopen.stderr, select.EPOLLIN)
if __name__ == '__main__':
run()
+87 -120
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -17,13 +17,11 @@
__author__ = 'alin37'
import asyncio
from getpass import getpass
import optparse
import os
import signal
import sys
import shlex
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
@@ -35,130 +33,99 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.asynclient as client
import confluent.client as client
async def main():
argparser = optparse.OptionParser(
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
\n %prog -c noderange <list of attributes> \
\n %prog -e noderange <attribute names to set> \
\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes are inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear attributes')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
argparser.add_option('-m', '--maxnodes', type='int',
help='Prompt if trying to set attributes on more '
'than specified number of nodes')
argparser.add_option('-s', '--set', dest='set', metavar='settings.batch',
default=False, help='set attributes using a batch file')
(options, args) = argparser.parse_args()
argparser = optparse.OptionParser(
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
\n %prog -c noderange <list of attributes> \
\n %prog -e noderange <attribute names to set> \
\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear attributes')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
(options, args) = argparser.parse_args()
#setting minimal output to only output current information
showtype = 'current'
requestargs=None
#setting minimal output to only output current information
showtype = 'current'
requestargs=None
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
#Sets attributes
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment or options.prompt:
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
exitcode=client.updateattrib(session,args,nodetype, noderange, options, argassign)
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
#Sets attributes
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment or options.prompt:
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
await session.stop_if_noderange_over(noderange, options.maxnodes)
exitcode = await client.updateattrib(session,args,nodetype, noderange, options, argassign)
try:
# setting user output to what the user inputs
if args[1] == 'all':
showtype = 'all'
requestargs=args[2:]
elif args[1] == 'current':
showtype = 'current'
requestargs=args[2:]
else:
showtype = 'all'
requestargs=args[1:]
except:
pass
elif options.clear or options.environment or options.prompt:
sys.stderr.write('Attribute names required with specified options\n')
argparser.print_help()
exitcode = 400
elif options.set:
arglist = [noderange]
showtype='current'
argfile = open(options.set, 'r')
argset = argfile.readline()
while argset:
try:
argset = argset[:argset.index('#')]
except ValueError:
pass
argset = argset.strip()
if argset:
arglist += shlex.split(argset)
argset = argfile.readline()
await session.stop_if_noderange_over(noderange, options.maxnodes)
exitcode= await client.updateattrib(session,arglist,nodetype, noderange, options, None)
if exitcode != 0:
sys.exit(exitcode)
# Lists all attributes
if len(args) > 0:
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
if requestargs is None:
# setting user output to what the user inputs
if args[1] == 'all':
showtype = 'all'
requestargs=args[2:]
elif args[1] == 'current':
showtype = 'current'
elif requestargs == []:
#showtype already set
pass
requestargs=args[2:]
else:
try:
requestargs.remove('all')
requestargs.remove('current')
except ValueError:
pass
exitcode = await client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print(res['item']['href'].replace('/', ''))
showtype = 'all'
requestargs=args[1:]
except:
pass
if exitcode != 0:
sys.exit(exitcode)
if __name__ == '__main__':
asyncio.run(main())
# Lists all attributes
if len(args) > 0:
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
if requestargs is None:
showtype = 'current'
elif requestargs == []:
#showtype already set
pass
else:
try:
requestargs.remove('all')
requestargs.remove('current')
except ValueError:
pass
exitcode = client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print res['item']['href'].replace('/', '')
sys.exit(exitcode)
-121
View File
@@ -1,121 +0,0 @@
#!/usr/libexec/platform-python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# 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.
__author__ = 'tkucherera'
from getpass import getpass
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> <username> <new_password>")
argparser.add_option('-m', '--maxnodes', type='int',
help='Number of nodes to affect before prompting for confirmation')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for password values interactively')
argparser.add_option('-e', '--environment', action='store_true',
help='Set password, but from environment variable of '
'same name')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
username = args[1]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
if options.prompt:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter pass for {0}: '.format(username))
twoval = getpass('Confirm pass for {0}: '.format(username))
if oneval != twoval:
print('Values did not match.')
new_password = twoval
elif len(args) == 3:
if options.environment:
key = args[2]
new_password = os.environ.get(key, os.environ[key.upper()])
else:
new_password = args[2]
else:
argparser.print_help()
sys.exit(1)
errorNodes = set([])
uid_dict = {}
session.stop_if_noderange_over(noderange, options.maxnodes)
for rsp in session.read('/noderange/{0}/configuration/management_controller/users/all'.format(noderange)):
databynode = rsp["databynode"]
for node in databynode:
if 'error' in rsp['databynode'][node]:
print(node, ':', rsp['databynode'][node]['error'])
errorNodes.add(node)
continue
for user in rsp['databynode'][node]['users']:
if user['username'] == username:
if user['uid'] not in uid_dict:
uid_dict[user['uid']] = node
continue
uid_dict[user['uid']] = uid_dict[user['uid']] + ',{}'.format(node)
break
if not uid_dict:
print("Error: Could not reach target node's bmc user")
sys.exit(1)
for uid in uid_dict:
success = session.simple_noderange_command(uid_dict[uid], 'configuration/management_controller/users/{0}'.format(uid), new_password, key='password', errnodes=errorNodes) # = 0 if successful
allNodes = set([])
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
if 'error' in node and success != 0:
sys.exit(success)
allNodes.add(node['item']['href'].replace("/", ""))
goodNodes = allNodes - errorNodes
for node in goodNodes:
print(node + ": Password Change Successful")
sys.exit(success)
+3 -7
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,8 +32,6 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
argparser.add_option('-m', '--maxnodes', type='int',
help='Number of nodes to affect before prompting for confirmation')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -45,7 +43,7 @@ session = client.Command()
exitcode = 0
errorNodes = set([])
session.stop_if_noderange_over(noderange, options.maxnodes)
success = session.simple_noderange_command(noderange, 'configuration/management_controller/reset', 'reset', key='state', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
@@ -53,14 +51,12 @@ success = session.simple_noderange_command(noderange, 'configuration/management_
allNodes = set([])
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
if 'error' in node and success != 0:
sys.exit(success)
allNodes.add(node['item']['href'].replace("/", ""))
goodNodes = allNodes - errorNodes
for node in goodNodes:
print(node + ": BMC Reset Successful")
print node + ": BMC Reset Successful"
sys.exit(success)
+3 -8
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
@@ -42,10 +42,6 @@ argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
'one time')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to boot, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
@@ -58,8 +54,8 @@ except IndexError:
sys.exit(1)
client.check_globbing(noderange)
bootdev = None
if len(args) > 1:
bootdev = args[1]
if len(sys.argv) > 2:
bootdev = sys.argv[2]
if bootdev in ('net', 'pxe'):
bootdev = 'network'
session = client.Command()
@@ -70,7 +66,6 @@ else:
bootmode = 'uefi'
errnodes = set([])
session.stop_if_noderange_over(noderange, options.maxnodes)
rc = session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist,
-104
View File
@@ -1,104 +0,0 @@
#!/usr/bin/python3
import os
import sys
from cryptography import x509
from cryptography.hazmat.primitives import hashes
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
def removebmccacert(noderange, certid, cmd):
for res in cmd.delete(f'/noderange/{noderange}/configuration/management_controller/certificate_authorities/{certid}'):
print(repr(res))
def listbmccacerts(noderange, cmd):
certids = []
for res in cmd.read(f'/noderange/{noderange}/configuration/management_controller/certificate_authorities'):
certids.append(res.get('item', {}).get('href', ''))
for certid in certids:
for res in cmd.read(f'/noderange/{noderange}/configuration/management_controller/certificate_authorities/{certid}'):
for node in res.get('databynode', {}):
certdata = res['databynode'][node].get('pem', {}).get('value', '')
summary = ''
if not certdata:
continue
san = res['databynode'][node].get('san', {}).get('value', '')
if san:
summary += f" SANs: {san}"
subject = res['databynode'][node].get('subject', {}).get('value', '')
if subject:
summary = subject
try:
cert = x509.load_pem_x509_certificate(certdata.encode())
sha256 = cert.fingerprint(hashes.SHA256()).hex().upper()
except Exception as e:
print(f"Error processing certificate for {node}: {e}", file=sys.stderr)
continue
summary += f" (SHA256={sha256})"
print(f"{node}: {certid}: {summary}")
def installbmccacert(noderange, certfile, cmd):
if certfile:
try:
with open(certfile, 'r') as f:
certdata = f.read()
except Exception as e:
print(f"Error reading certificate file: {e}", file=sys.stderr)
sys.exit(1)
# Simple validation: check if it starts and ends with the correct PEM markers
if not (certdata.startswith("-----BEGIN CERTIFICATE-----") and certdata.strip().endswith("-----END CERTIFICATE-----")):
print("Invalid certificate format. Must be a PEM encoded certificate.", file=sys.stderr)
sys.exit(1)
payload = {'pem': certdata}
for res in cmd.update(f'/noderange/{noderange}/configuration/management_controller/certificate_authorities', payload):
print(repr(res))
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Node certificate utility')
parser.add_argument('noderange', help='Node range to operate on')
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# installbmccacert subcommand
install_parser = subparsers.add_parser('installbmccacert', help='Install BMC CA certificate')
install_parser.add_argument('filename', help='Certificate file to install')
remove_parser = subparsers.add_parser('removebmccacert', help='Remove BMC CA certificate')
remove_parser.add_argument('id', help='Certificate id to remove')
list_parser = subparsers.add_parser('listbmccacerts', help='List BMC CA certificates')
sign_bmc_parser = subparsers.add_parser('signbmccert', help='Sign BMC certificate')
sign_bmc_parser.add_argument('--days', type=int, help='Number of days the certificate is valid for')
sign_bmc_parser.add_argument('--added-names', type=str, help='Additional names to include in the certificate')
args = parser.parse_args()
c = client.Command()
if args.command == 'installbmccacert':
installbmccacert(args.noderange, args.filename, c)
elif args.command == 'removebmccacert':
removebmccacert(args.noderange, args.id, c)
elif args.command == 'listbmccacerts':
listbmccacerts(args.noderange, c)
elif args.command == 'signbmccert':
payload = {}
if args.days is not None:
payload['days'] = args.days
else:
print("Error: --days is required for signbmccert", file=sys.stderr)
sys.exit(1)
if args.added_names:
payload['added_names'] = args.added_names
for res in c.update(f'/noderange/{args.noderange}/configuration/management_controller/certificate/sign', payload):
print(repr(res))
else:
parser.print_help()
sys.exit(1)
+96 -171
View File
@@ -1,7 +1,7 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2025 Lenovo
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import os
import signal
import optparse
@@ -31,7 +31,7 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.asynclient as client
import confluent.client as client
class NullOpt(object):
blame = None
@@ -43,7 +43,7 @@ def bailout(msg, code=1):
sys.exit(code)
argparser = optparse.OptionParser(usage="Usage: %prog [options] <noderange> [setting|setting=value]")
argparser = optparse.OptionParser(usage="Usage: %prog [options] noderange [option|option=value]")
argparser.add_option('-c', '--comparedefault', dest='comparedefault',
action='store_true', default=False,
help='Compare given settings to default or list settings '
@@ -54,15 +54,9 @@ argparser.add_option('-d', '--detail', dest='detail',
action='store_true', default=False,
help='Provide verbose information as available, such as '
'help text and possible valid values')
argparser.add_option('-e', '--extra', dest='extra',
action='store_true', default=False,
help='Access extra configuration. Extra configuration is generally '
'reserved for unpopular or redundant options that may be slow to '
'read. Notably the IMM category on Lenovo settings is considered '
'to be extra configuration')
argparser.add_option('-x', '--exclude', dest='exclude',
action='store_true', default=False,
help='Treat named settings as items to not '
help='Treat positional arguments as items to not '
'examine, compare, or restore default')
argparser.add_option('-a', '--advanced', dest='advanced',
action='store_true', default=False,
@@ -73,14 +67,7 @@ argparser.add_option('-r', '--restoredefault', default=False,
dest='restoredefault', metavar="COMPONENT",
help='Restore the configuration of the node '
'to factory default for given component. '
'Currently "uefi" and "bmc" are supported components')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to configure, '
'prompting if over the threshold')
argparser.add_option('-p', '--pending', dest='showpending',
action='store_true', default=False,
help='Show pending changes for attributes')
'Currently only uefi is supported')
(options, args) = argparser.parse_args()
cfgpaths = {
@@ -93,18 +80,6 @@ cfgpaths = {
'bmc.ipv4_gateway': (
'configuration/management_controller/net_interfaces/management',
'ipv4_gateway'),
'bmc.static_ipv6_addresses': (
'configuration/management_controller/net_interfaces/management',
'static_v6_addresses'),
'bmc.static_ipv6_gateway': (
'configuration/management_controller/net_interfaces/management',
'static_v6_gateway'),
'bmc.vlan_id': (
'configuration/management_controller/net_interfaces/management',
'vlan_id'),
'bmc.mac_address': (
'configuration/management_controller/net_interfaces/management',
'hw_addr'),
'bmc.hostname': (
'configuration/management_controller/hostname', 'hostname'),
}
@@ -123,9 +98,6 @@ setmode = None
assignment = {}
queryparms = {}
printsys = []
printbmc = []
printextbmc = []
printallbmc = False
setsys = {}
forceset = False
needval = None
@@ -149,10 +121,9 @@ def _assign_value():
assignment[key] = value
def parse_config_line(arguments, single=False):
global setmode, printallbmc, forceset, key, value, needval, candidate, path, attrib
for pidx in range(0, len(arguments)):
param = arguments[pidx]
def parse_config_line(arguments):
global setmode, forceset, key, value, needval, candidate, path, attrib
for param in arguments:
if param == 'show':
continue # forgive muscle memory of pasu users
if param == 'set':
@@ -162,19 +133,14 @@ def parse_config_line(arguments, single=False):
if needval:
key = needval
needval = None
if single:
value = ' '.join(arguments[pidx:])
_assign_value()
break
else:
value = param
value = param
_assign_value()
continue
if '=' in param or param[-1] == ':' or forceset:
if setmode is None:
setmode = True
if not setmode:
bailout('Cannot do set and query in same command: Query detected but "{0}" appears to be set'.format(param))
if setmode != True:
bailout('Cannot do set and query in same command')
if '=' in param:
key, _, value = param.partition('=')
_assign_value()
@@ -185,11 +151,9 @@ def parse_config_line(arguments, single=False):
else:
if setmode is None:
setmode = False
if setmode:
bailout('Cannot do set and query in same command: Set mode detected but "{0}" appears to be a query'.format(param))
if setmode != False:
bailout('Cannot do set and query in same command')
if '.' not in param:
if param == 'bmc':
printallbmc = True
matchedparms = False
for candidate in cfgpaths:
if candidate.startswith('{0}.'.format(param)):
@@ -205,19 +169,10 @@ def parse_config_line(arguments, single=False):
del queryparms[path]
except KeyError:
pass
if param.lower() == 'imm':
printextbmc.append(param)
options.extra = True
elif not matchedparms:
if not matchedparms:
printsys.append(param)
elif param not in cfgpaths:
if param.startswith('bmc.'):
printbmc.append(param.replace('bmc.', ''))
elif param.lower().startswith('imm'):
options.extra = True
printextbmc.append(param)
else:
printsys.append(param)
printsys.append(param)
else:
path, attrib = cfgpaths[param]
path = '/noderange/{0}/{1}'.format(noderange, path)
@@ -225,114 +180,84 @@ def parse_config_line(arguments, single=False):
queryparms[path] = {}
queryparms[path][attrib] = param
async def main():
global printsys
if options.batch:
printsys = []
argfile = open(options.batch, 'r')
if options.batch:
printsys = []
argfile = open(options.batch, 'r')
argset = argfile.readline()
while argset:
try:
argset = argset[:argset.index('#')]
except ValueError:
pass
argset = argset.strip()
if argset:
parse_config_line(shlex.split(argset))
argset = argfile.readline()
while argset:
try:
argset = argset[:argset.index('#')]
except ValueError:
pass
argset = argset.strip()
if argset:
parse_config_line(shlex.split(argset), single=True)
argset = argfile.readline()
else:
parse_config_line(args[1:])
session = client.Command()
rcode = 0
if options.restoredefault:
await session.stop_if_noderange_over(noderange, options.maxnodes)
if options.restoredefault.lower() in (
'sys', 'system', 'uefi', 'bios'):
async for fr in session.update(
'/noderange/{0}/configuration/system/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
elif options.restoredefault.lower() in (
'bmc', 'imm', 'xcc'):
async for fr in session.update(
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
else:
sys.stderr.write(
'Unrecognized component to restore defaults: {0}\n'.format(
options.restoredefault))
sys.exit(1)
if setmode:
await session.stop_if_noderange_over(noderange, options.maxnodes)
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
sys.exit(1)
updatebypath = {}
attrnamebypath = {}
for key in assignment:
if key not in cfgpaths:
if key.startswith('bmc.'):
path = 'configuration/management_controller/extended/all'
attrib = key.replace('bmc.', '')
else:
path = 'configuration/system/all'
attrib = key
else:
path, attrib = cfgpaths[key]
if path not in updatebypath:
updatebypath[path] = {}
attrnamebypath[path] = {}
updatebypath[path][attrib] = assignment[key]
attrnamebypath[path][attrib] = key
# well, we want to expand things..
# check ipv4, if requested change method to static
for path in updatebypath:
async for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
updatebypath[path]):
rcode |= client.printerror(fr)
for node in fr.get('databynode', []):
r = fr['databynode'][node]
if 'value' not in r:
continue
keyval = r['value']
key, val = keyval.split('=')
if key in attrnamebypath[path]:
key = attrnamebypath[path][key]
print('{0}: {1}: {2}'.format(node, key, val))
else:
for path in queryparms:
if options.comparedefault:
continue
rcode |= await client.print_attrib_path(path, session, list(queryparms[path]),
NullOpt(), queryparms[path], showpending=options.showpending)
if printsys == 'all' or printextbmc or printbmc or printallbmc:
if printbmc or not printextbmc:
rcode |= await client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
session, printbmc, options, attrprefix='bmc.', showpending=options.showpending)
if options.extra:
if options.advanced:
rcode |= await client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
session, printextbmc, options, showpending=options.showpending)
else:
rcode |= await client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
session, printextbmc, options, showpending=options.showpending)
if printsys or options.exclude:
if printsys == 'all':
printsys = []
if (options.comparedefault or printsys == []) and not options.advanced:
path = '/noderange/{0}/configuration/system/all'.format(noderange)
else:
path = '/noderange/{0}/configuration/system/advanced'.format(
noderange)
rcode |= await client.print_attrib_path(path, session, printsys,
options, showpending=options.showpending)
else:
parse_config_line(args[1:])
session = client.Command()
rcode = 0
if options.restoredefault and options.restoredefault.lower() in (
'sys', 'system', 'uefi', 'bios'):
for fr in session.update(
'/noderange/{0}/configuration/system/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
if __name__ == '__main__':
asyncio.run(main())
elif options.restoredefault:
sys.stderr.write(
'Unrecognized component to restore defaults: {0}\n'.format(
options.restoredefault))
sys.exit(1)
if setmode:
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
sys.exit(1)
updatebypath = {}
attrnamebypath = {}
for key in assignment:
if key not in cfgpaths:
path = 'configuration/system/all'
attrib = key
else:
path, attrib = cfgpaths[key]
if path not in updatebypath:
updatebypath[path] = {}
attrnamebypath[path] = {}
updatebypath[path][attrib] = assignment[key]
attrnamebypath[path][attrib] = key
# well, we want to expand things..
# check ipv4, if requested change method to static
for path in updatebypath:
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
updatebypath[path]):
rcode |= client.printerror(fr)
for node in fr.get('databynode', []):
r = fr['databynode'][node]
rcode |= client.printerror(r, node)
if 'value' not in r:
continue
keyval = r['value']
key, val = keyval.split('=')
if key in attrnamebypath[path]:
key = attrnamebypath[path][key]
print('{0}: {1}: {2}'.format(node, key, val))
else:
for path in queryparms:
if options.comparedefault:
continue
rc = client.print_attrib_path(path, session, list(queryparms[path]),
NullOpt(), queryparms[path])
if rc:
sys.exit(rc)
if printsys or options.exclude:
if printsys == 'all':
printsys = []
if (options.comparedefault or printsys == []) and not options.advanced:
path = '/noderange/{0}/configuration/system/all'.format(noderange)
else:
path = '/noderange/{0}/configuration/system/advanced'.format(
noderange)
rcode = client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
File diff suppressed because it is too large Load Diff
+26 -36
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -15,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import optparse
import os
import signal
@@ -31,38 +30,29 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.asynclient as client
import confluent.client as client
async def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs = None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
if '=' not in arg:
sys.stderr.write(
'Attributes must be given as attribute=value, got "{0}"\n'.format(
arg))
sys.exit(1)
key, val = arg.split('=', 1)
attribs[key] = val
async for r in session.create('/noderange/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
if __name__ == '__main__':
asyncio.run(main())
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs=None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/noderange/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
-236
View File
@@ -1,236 +0,0 @@
#!/usr/bin/python3
import argparse
import datetime
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
def cleararm(nr, cli):
nodes = set([])
for rsp in cli.read('/noderange/{0}/attributes/current'.format(nr)):
for node in rsp.get('databynode', {}):
nodeinfo = rsp['databynode'][node]
for attr in nodeinfo:
if attr == 'deployment.apiarmed':
curr = nodeinfo[attr].get('value', '')
if curr == 'continuous':
nodes.add(node)
noderange = nr
if nodes:
noderange += ',-({0})'.format(','.join(nodes))
for rsp in cli.update('/noderange/{0}/attributes/current'.format(noderange),
{'deployment.apiarmed': ''}):
pass
def armonce(nr, cli):
nodes = set([])
for rsp in cli.read('/noderange/{0}/attributes/current'.format(nr)):
for node in rsp.get('databynode', {}):
nodeinfo = rsp['databynode'][node]
for attr in nodeinfo:
if attr == 'deployment.apiarmed':
curr = nodeinfo[attr].get('value', '')
if curr == 'continuous':
nodes.add(node)
noderange = nr
if nodes:
noderange += ',-({0})'.format(','.join(nodes))
for rsp in cli.update('/noderange/{0}/attributes/current'.format(noderange),
{'deployment.apiarmed': 'once'}):
pass
def setpending(nr, profile, profilebynodes, cli):
if profilebynodes:
for node in sortutil.natural_sort(profilebynodes):
prof = profilebynodes[node]
args = {'deployment.pendingprofile': prof, 'deployment.state': '', 'deployment.state_detail': '', 'deployment.state_last_updated': ''}
if not prof.startswith('genesis-'):
args['deployment.stagedprofile'] = ''
args['deployment.profile'] = ''
for rsp in cli.update('/nodes/{0}/attributes/current'.format(node),
args):
pass
return
args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': '', 'deployment.state_last_updated': ''}
if not profile.startswith('genesis-'):
args['deployment.stagedprofile'] = ''
args['deployment.profile'] = ''
for rsp in cli.update('/noderange/{0}/attributes/current'.format(nr),
args):
pass
def clearpending(nr, cli):
for rsp in cli.update('/noderange/{0}/attributes/current'.format(nr),
{'deployment.pendingprofile': ''}):
pass
def main(args):
ap = argparse.ArgumentParser(description='Deploy OS to nodes')
ap.add_argument('-c', '--clear', help='Clear any pending deployment action', action='store_true')
ap.add_argument('-n', '--network', help='Initiate deployment over PXE/HTTP', action='store_true')
ap.add_argument('-b', '--bootmethod', help='Specify network boot method (e.g., network, http)', default='network')
ap.add_argument('-p', '--prepareonly', help='Prepare only, skip any interaction with a BMC associated with this deployment action', action='store_true')
ap.add_argument('-m', '--maxnodes', help='Specify a maximum nodes to be deployed')
ap.add_argument('-r', '--redeploy', help='Redeploy nodes with the current or pending profile', action='store_true')
ap.add_argument('noderange', help='Set of nodes to deploy')
ap.add_argument('profile', nargs='?', help='Profile name to deploy')
args, extra = ap.parse_known_args(args)
if args.profile is None and len(extra) == 1:
args.profile = extra[0]
extra = extra[1:]
if args.profile and not args.network:
sys.stderr.write('-n is a required argument currently to perform an install, optionally with -p\n')
return 1
if not args.profile and args.network and not args.redeploy:
sys.stderr.write('Both noderange and a profile name are required arguments to request a network deployment\n')
return 1
if args.clear and args.profile:
sys.stderr.write(
'The -c/--clear option should not be used with a profile, '
'it is a request to not deploy any profile, and will clear '
'whatever the current profile is without being specified\n')
return 1
if extra:
sys.stderr.write('Unrecognized arguments: ' + repr(extra) + '\n')
c = client.Command()
c.stop_if_noderange_over(args.noderange, args.maxnodes)
errnodes = set([])
for rsp in c.read('/noderange/{0}/nodes/'.format(args.noderange)):
if 'error' in rsp:
sys.stderr.write(rsp['error'] + '\n')
sys.exit(1)
profilebynode = {}
if args.clear:
cleararm(args.noderange, c)
clearpending(args.noderange, c)
elif args.redeploy:
hadpending = {}
for rsp in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
for node in rsp.get('databynode', {}):
nodeinfo = rsp['databynode'][node]
for attr in nodeinfo:
if attr == 'deployment.pendingprofile':
curr = nodeinfo[attr].get('value', '')
if curr:
hadpending[node] = True
profilebynode[node] = curr
if attr == 'deployment.stagedprofile':
curr = nodeinfo[attr].get('value', '')
if curr and node not in hadpending:
profilebynode[node] = curr
if attr == 'deployment.profile':
curr = nodeinfo[attr].get('value', '')
if curr and node not in profilebynode:
profilebynode[node] = curr
if args.profile and profilebynode:
sys.stderr.write('The -r/--redeploy option cannot be used with a profile, it redeploys the current or pending profile\n')
return 1
if args.profile or profilebynode:
lockednodes = []
for lockinfo in c.read('/noderange/{0}/deployment/lock'.format(args.noderange)):
for node in lockinfo.get('databynode', {}):
lockstate = lockinfo['databynode'][node]['lock']['value']
if lockstate == 'locked':
lockednodes.append(node)
if lockednodes:
sys.stderr.write('Requested noderange has nodes with locked deployment: ' + ','.join(lockednodes))
sys.stderr.write('\n')
sys.exit(1)
if args.profile:
profnames = []
for prof in c.read('/deployment/profiles/'):
profname = prof.get('item', {}).get('href', None)
if profname:
profname = profname.replace('/', '')
profnames.append(profname)
if profname == args.profile:
break
else:
sys.stderr.write('The specified profile "{}" is not an available profile\n'.format(args.profile))
if profnames:
sys.stderr.write('The following profiles are available:\n')
for profname in profnames:
sys.stderr.write(' ' + profname + '\n')
else:
sys.stderr.write('No deployment profiles available, try osdeploy import or imgutil capture\n')
sys.exit(1)
armonce(args.noderange, c)
setpending(args.noderange, args.profile, profilebynode, c)
else:
databynode = {}
for r in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
dbn = r.get('databynode', {})
for node in dbn:
if node not in databynode:
databynode[node] = {}
for attr in dbn[node]:
if attr in ('deployment.pendingprofile', 'deployment.apiarmed', 'deployment.stagedprofile', 'deployment.profile', 'deployment.state', 'deployment.state_detail', 'deployment.state_last_updated'):
databynode[node][attr] = dbn[node][attr].get('value', '')
for node in sortutil.natural_sort(databynode):
profile = databynode[node].get('deployment.pendingprofile', '')
if profile:
profile = 'pending: {}'.format(profile)
else:
profile = databynode[node].get('deployment.stagedprofile', '')
if profile:
profile = 'staged: {}'.format(profile)
else:
profile = databynode[node].get('deployment.profile', '')
if profile:
profile = 'completed: {}'.format(profile)
elif args.clear:
profile = 'No profile applied, any pending profile has been cleared'
else:
profile= 'No profile pending or applied'
armed = databynode[node].get('deployment.apiarmed', '')
if armed in ('once', 'continuous'):
armed = ' (node authentication armed)'
else:
armed = ''
stateinfo = ''
deploymentstate = databynode[node].get('deployment.state', '')
if deploymentstate:
deploymentdate = databynode[node].get('deployment.state_last_updated', '')
if deploymentdate:
try:
deploymentdate = datetime.datetime.fromisoformat(deploymentdate).strftime('%m/%d/%Y %H:%M:%S')
except ValueError:
pass
statedetails = databynode[node].get('deployment.state_detail', '')
if statedetails:
stateinfo = '{}: {}'.format(deploymentstate, statedetails)
else:
stateinfo = deploymentstate
if deploymentdate:
stateinfo += ' - {}'.format(deploymentdate)
if stateinfo:
print('{0}: {1} ({2})'.format(node, profile, stateinfo))
else:
print('{0}: {1}{2}'.format(node, profile, armed))
sys.exit(0)
if not args.clear and args.network and not args.prepareonly:
rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', args.bootmethod,
bootmode='uefi',
persistent=False,
errnodes=errnodes)
if errnodes:
sys.stderr.write(
'Unable to set boot device for following nodes: {0}\n'.format(
','.join(errnodes)))
return 1
rc |= c.simple_noderange_command(args.noderange, '/power/state', 'boot')
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
+63 -169
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -15,25 +15,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import csv
import optparse
import os
import sys
import time
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.asynclient as client
import confluent.client as client
import confluent.sortutil as sortutil
defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
'Current IP Addresses']
columnmapping = {
'Node': 'nodename',
'Model': 'modelnumber',
'Model Name': 'modelname',
'Serial': 'serialnumber',
'UUID': 'uuid',
'Type': 'types',
@@ -47,54 +47,20 @@ columnmapping = {
'Advertised IP': 'otheripaddrs',
'Other IP': 'otheripaddrs',
}
#TODO: add chassis uuid
async def register_endpoint(options, session, addr):
neednewline = False
current = 0
total = 0
async for rsp in session.update('/discovery/register', {'addresses': addr}):
if 'count' in rsp:
total = rsp['count']
elif total > 1:
current += 1
sys.stdout.write('\x1b[2K\r{0:.1f}% Scanned'.format(100 * current / total))
sys.stdout.flush()
neednewline = True
if 'created' in rsp:
if neednewline:
print('')
neednewline = False
print('Registered: {0}'.format(rsp['created']))
if neednewline:
print('')
async def subscribe_discovery(options, session, subscribe, targ):
keyn = 'subscribe' if subscribe else 'unsubscribe'
payload = {keyn: targ}
if subscribe:
async for rsp in session.update('/discovery/subscriptions/{0}'.format(targ), payload):
if 'status' in rsp:
print(rsp['status'])
else:
async for rsp in session.delete('/discovery/subscriptions/{0}'.format(targ)):
if 'status' in rsp:
print(rsp['status'])
async def print_disco(options, session, currid, outhandler, columns):
def print_disco(options, session, currmac, outhandler, columns):
procinfo = {}
async for tmpinfo in session.read('/discovery/by-id/{0}'.format(currid)):
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
procinfo.update(tmpinfo)
if 'Switch' in columns or 'Port' in columns:
if 'switch' in procinfo:
procinfo['port'] = procinfo['switchport']
elif ':' in currid:
async for tmpinfo in session.read(
'/networking/macs/by-mac/{0}'.format(currid)):
if 'ports' in tmpinfo:
# The api sorts so that the most specific available value
# is last
procinfo.update(tmpinfo['ports'][-1])
for tmpinfo in session.read(
'/networking/macs/by-mac/{0}'.format(currmac)):
if 'ports' in tmpinfo:
# The api sorts so that the most specific available value
# is last
procinfo.update(tmpinfo['ports'][-1])
record = []
for col in columns:
rawcol = columnmapping[col]
@@ -122,12 +88,10 @@ def process_header(header):
fields.append('serial')
elif datum == 'uuid':
fields.append('uuid')
elif datum in ('bmc', 'imm', 'xcc', 'ip'):
elif datum in ('bmc', 'imm', 'xcc'):
fields.append('hardwaremanagement.manager')
elif datum in ('bmc gateway', 'xcc gateway', 'imm gateway'):
fields.append('net.bmc.ipv4_gateway')
elif datum in ('bmc_gateway', 'xcc_gateway', 'imm_gateway'):
fields.append('net.bmc.ipv4_gateway')
elif datum in ('bmcuser', 'username', 'user'):
fields.append('secret.hardwaremanagementuser')
elif datum in ('bmcpass', 'password', 'pass'):
@@ -159,12 +123,12 @@ def datum_complete(datum):
searchkeys = set(['mac', 'serial', 'uuid'])
async def search_record(datum, options, session):
def search_record(datum, options, session):
for searchkey in searchkeys:
options.__dict__[searchkey] = None
for searchkey in searchkeys & set(datum):
options.__dict__[searchkey] = datum[searchkey]
return [x async for x in list_matching_ents(options, session)]
return list(list_matching_macs(options, session))
def datum_to_attrib(datum):
@@ -179,21 +143,15 @@ def datum_to_attrib(datum):
unique_fields = frozenset(['serial', 'mac', 'uuid'])
# Cap how many nodes hold a discovery session at once while importing
maxconcurrentassign = 128
async def import_csv(options, session):
def import_csv(options, session):
nodedata = []
unique_data = {}
exitcode = 0
with open(options.importfile, 'r') as datasrc:
records = csv.reader(datasrc)
fields = process_header(next(records))
for field in fields:
if field in unique_fields:
unique_data[field] = set([])
broken = False
alldata=[]
for record in records:
currfields = list(fields)
nodedatum = {}
@@ -210,69 +168,39 @@ async def import_csv(options, session):
nodedatum[currfield] = datum
if not datum_complete(nodedatum):
sys.exit(1)
alldata.append(nodedatum)
allthere = True
for nodedatum in alldata:
if not await search_record(nodedatum, options, session) and not broken:
allthere = False
await blocking_scan(session)
break
for nodedatum in alldata:
if not allthere and not await search_record(nodedatum, options, session):
if not search_record(nodedatum, options, session):
blocking_scan(session)
if not search_record(nodedatum, options, session):
sys.stderr.write(
"Could not match the following data: " +
repr(nodedatum) + '\n')
broken = True
sys.exit(1)
nodedata.append(nodedatum)
if broken:
sys.exit(1)
assignments = []
assignlimit = asyncio.Semaphore(maxconcurrentassign)
for datum in nodedata:
maclist = await search_record(datum, options, session)
maclist = search_record(datum, options, session)
datum = datum_to_attrib(datum)
nodename = datum['name']
async for res in session.create('/nodes/', datum):
for res in session.create('/nodes/', datum):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
continue
elif 'created' in res:
print('Defined ' + res['created'])
else:
print(repr(res))
assignments.append(
asyncio.create_task(assign_macs(maclist, nodename, assignlimit)))
for rcode in await asyncio.gather(*assignments, return_exceptions=True):
if isinstance(rcode, BaseException):
sys.stderr.write('Error assigning discovery data: {0}\n'.format(rcode))
rcode = 1
exitcode |= rcode
if exitcode:
sys.exit(exitcode)
async def assign_macs(maclist, nodename, assignlimit):
exitcode = 0
async with assignlimit:
# A session of our own, since the connection carries one request at a
# time and the caller's is busy defining the remaining nodes
mysess = client.Command()
for mac in maclist:
async for res in mysess.update('/discovery/by-id/{0}'.format(mac),
{'node': nodename}):
for res in session.update('/discovery/by-mac/{0}'.format(mac),
{'node': nodename}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
continue
elif 'assigned' in res:
print('Discovered ' + res['assigned'])
else:
print(repr(res))
return exitcode
async def list_discovery(options, session):
def list_discovery(options, session):
orderby = None
if options.fields:
columns = []
@@ -288,29 +216,24 @@ async def list_discovery(options, session):
if options.order.lower() == field.lower():
orderby = field
outhandler = client.Tabulator(columns)
for infoid in [x async for x in list_matching_ents(options, session)]:
await print_disco(options, session, infoid, outhandler, columns)
for mac in list_matching_macs(options, session):
print_disco(options, session, mac, outhandler, columns)
if options.csv:
outhandler.write_csv(sys.stdout, orderby)
else:
for row in outhandler.get_table(orderby):
print(row)
async def clear_discovery(options, session):
allidentifiers = [x async for x in list_matching_ents(options, session)]
for infoid in allidentifiers:
async for res in session.delete('/discovery/by-id/{0}'.format(infoid)):
def clear_discovery(options, session):
for mac in list_matching_macs(options, session):
for res in session.delete('/discovery/by-mac/{0}'.format(mac)):
if 'deleted' in res:
print('Cleared info for {0}'.format(res['deleted']))
else:
print(repr(res))
async def list_matching_ents(options, session, node=None, checknode=True):
def list_matching_macs(options, session):
path = '/discovery/'
if node:
path += 'by-node/{0}/'.format(node)
elif checknode and options.node:
path += 'by-node/{0}/'.format(options.node)
if options.model:
path += 'by-model/{0}/'.format(options.model)
if options.serial:
@@ -324,66 +247,54 @@ async def list_matching_ents(options, session, node=None, checknode=True):
options.state = 'unidentified'
path += 'by-state/{0}/'.format(options.state).lower()
if options.mac:
path += 'by-id/{0}'.format(options.mac)
result = list([x async for x in session.read(path)])[0]
path += 'by-mac/{0}'.format(options.mac)
result = list(session.read(path))[0]
if 'error' in result:
return
yield options.mac.replace(':', '-')
return
return []
return [options.mac.replace(':', '-')]
else:
path += 'by-id/'
async for x in session.read(path):
if 'item' in x and 'href' in x['item']:
yield x['item']['href']
path += 'by-mac/'
return [x['item']['href'] for x in session.read(path)]
async def assign_discovery(options, session, needid=True):
def assign_discovery(options, session):
abort = False
if options.importfile:
return await import_csv(options, session)
if not options.node:
sys.stderr.write("Node (-n) must be specified for assignment\n")
abort = True
if needid and not (options.serial or options.uuid or options.mac):
return import_csv(options, session)
if not (options.serial or options.uuid or options.mac):
sys.stderr.write(
"UUID (-u), serial (-s), or ether address (-e) required for "
"assignment\n")
abort = True
if not options.node:
sys.stderr.write("Node (-n) must be specified for assignment\n")
abort = True
if abort:
sys.exit(1)
matches = [x async for x in list_matching_ents(options, session, None if needid else options.node, False)]
matches = list_matching_macs(options, session)
if not matches:
# Do a rescan to catch missing requested data
if needid and options.mac and '.' in options.mac:
await register_endpoint(options, session, options.mac)
else:
await blocking_scan(session)
matches = [x async for x in list_matching_ents(options, session, None if needid else options.node, False)]
blocking_scan(session)
matches = list_matching_macs(options, session)
if not matches:
sys.stderr.write("No matching discovery candidates found\n")
sys.exit(1)
exitcode = 0
async for res in session.update('/discovery/by-id/{0}'.format(matches[0]),
for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
{'node': options.node}):
if 'assigned' in res:
print('Assigned: {0}'.format(res['assigned']))
elif 'error' in res:
sys.stderr.write('Error: {0}\n'.format(res['error']))
exitcode |= res.get('errorcode', 1)
else:
print(repr(res))
if exitcode:
sys.exit(exitcode)
async def blocking_scan(session, aggressive=False):
list([x async for x in session.update('/discovery/rescan', {'rescan': 'aggressive' if aggressive else 'start'})])
while(list([x async for x in session.read('/discovery/rescan')])[0].get('scanning', False)):
await asyncio.sleep(0.5)
list([x async for x in session.update('/networking/macs/rescan', {'rescan': 'start'})])
def blocking_scan(session):
list(session.update('/discovery/rescan', {'rescan': 'start'}))
while(list(session.read('/discovery/rescan'))[0].get('scanning', False)):
time.sleep(0.5)
list(session.update('/networking/macs/rescan', {'rescan': 'start'}))
async def main():
def main():
parser = optparse.OptionParser(
usage='Usage: %prog [list|assign|rescan|clear|subscribe|unsubscribe|register] [options]')
usage='Usage: %prog [list|assign|rescan|clear] [options]')
# -a for 'address' maybe?
# order by
# show state (discovered or..
@@ -391,9 +302,6 @@ async def main():
# flush to clear old data out? (e.g. no good way to age pxe data)
# also delete discovery datum... more targeted
# defect: -t lenovo-imm returns all
parser.add_option('-a', '--aggressive', dest='aggressive',
help='Use aggressive scanning and fingerprinting that is slower and more intrusive',
action='store_true')
parser.add_option('-m', '--model', dest='model',
help='Operate with nodes matching the specified model '
'number', metavar='MODEL')
@@ -408,8 +316,8 @@ async def main():
'UUID', metavar='UUID')
parser.add_option('-n', '--node', help='Operate with the given nodename')
parser.add_option('-e', '--ethaddr', dest='mac',
help='Operate against the system with the specified MAC or IP '
'address if mac not available', metavar='MAC')
help='Operate against the system with the specified MAC '
'address', metavar='MAC')
parser.add_option('-t', '--type', dest='type',
help='Operate against the system of the specified type',
metavar='TYPE')
@@ -424,34 +332,20 @@ async def main():
parser.add_option('-o', '--order', dest='order',
help='Order output by given field', metavar='ORDER')
(options, args) = parser.parse_args()
if len(args) == 0 or args[0] not in ('list', 'assign', 'reassign', 'rescan', 'clear', 'subscribe', 'unsubscribe', 'register'):
if len(args) == 0 or args[0] not in ('list', 'assign', 'rescan', 'clear'):
parser.print_help()
sys.exit(1)
if args[0] == 'register' and len(args) != 2:
sys.stderr.write('Register requires target address or range (e.g. 192.168.2.30, 192.168.2.0/24, or 192.168.2.1-192.168.2.30)\n')
sys.exit(1)
if 'subscribe' in args[0] and len(args) != 2:
sys.stderr.write('subscribe/unsubscribe subcommands require switch name as argument\n')
sys.exit(1)
session = client.Command()
if args[0] == 'list':
await list_discovery(options, session)
list_discovery(options, session)
if args[0] == 'clear':
await clear_discovery(options, session)
clear_discovery(options, session)
if args[0] == 'assign':
await assign_discovery(options, session)
if args[0] == 'reassign':
await assign_discovery(options, session, False)
if args[0] == 'register':
await register_endpoint(options, session, args[1])
if args[0] == 'subscribe':
await subscribe_discovery(options, session, True, args[1])
if args[0] == 'unsubscribe':
await subscribe_discovery(options, session, False, args[1])
assign_discovery(options, session)
if args[0] == 'rescan':
await blocking_scan(session, aggressive=options.aggressive)
blocking_scan(session)
print("Rescan complete")
if __name__ == '__main__':
asyncio.run(main())
main()
+14 -147
View File
@@ -1,7 +1,7 @@
#!/usr/bin/python2
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2019 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
import codecs
from datetime import datetime as dt
from datetime import timedelta
import optparse
import os
import signal
@@ -34,32 +33,10 @@ if path.startswith('/opt'):
import confluent.client as client
if sys.version_info[0] < 3:
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
argparser = optparse.OptionParser(
usage="Usage: %prog [options] <noderange> [clear]")
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to clear if clearing log, '
'prompting if over the threshold')
argparser.add_option('-l', '--lines', type='int',
help='return the last <n> entries '
'for each node in the eventlog. '
)
argparser.add_option('-t', '--timeframe', type='string',
help='return entries within a specified timeframe '
'for each node in the eventlog. This will return '
'entries from the last hours or days. '
'1h would be one hour, 4d would be four days. '
'format <num>h or <num>d'
)
argparser.add_option('-s', '--source', type='string',
help='only show entries from the named logs, comma '
'delimited. "-s list" names the logs this node '
'offers instead of showing entries')
usage="Usage: %prog [options] noderange [clear]")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -68,43 +45,25 @@ except IndexError:
sys.exit(1)
client.check_globbing(noderange)
deletemode = False
if len(args) > 2:
if len(sys.argv) > 3:
argparser.print_help()
sys.exit(1)
if len(args) == 2:
if args[1] == 'clear':
if len(sys.argv) == 3:
if sys.argv[2] == 'clear':
deletemode = True
else:
argparser.print_help()
sys.exit(1)
listmode = bool(options.source) and options.source.lower() == 'list'
wantsources = None
if options.source and not listmode:
wantsources = set(x.strip().lower() for x in options.source.split(',')
if x.strip())
if options.source and deletemode:
# Clearing a subset is not something the platforms offer, and clearing more
# than was asked for is not something to do quietly
sys.stderr.write(
'Clearing a selection of logs is not supported, so --source cannot be '
'combined with clear\n')
sys.exit(1)
session = client.Command()
exitcode = 0
def format_event(evt):
retparts = []
if 'timestamp' in evt and evt['timestamp'] is not None:
try:
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
except ValueError:
display = ''
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
dscparts = []
if evt.get('log_id', None):
retparts.append(evt['log_id'] + ':')
if 'component_type' in evt and evt['component_type'] is not None:
dscparts.append(evt['component_type'])
if 'component' in evt and evt['component'] is not None:
@@ -118,40 +77,13 @@ def format_event(evt):
pass
dscparts.append(evttext)
retparts.append(' - '.join(dscparts))
msg = evt.get('message')
if not msg:
msg = ''
return ' '.join(retparts) + msg
return ' '.join(retparts)
if deletemode:
func = session.delete
session.stop_if_noderange_over(noderange, options.maxnodes)
else:
func = session.read
if options.timeframe:
try:
delta = int(options.timeframe[:-1])
except ValueError:
argparser.print_help()
sys.exit(1)
if options.timeframe[-1].lower() == 'd':
tdelta = timedelta(days=delta)
elif options.timeframe[-1].lower() == 'h':
tdelta = timedelta(hours=delta)
else:
argparser.print_help()
sys.exit(1)
timeframe = dt.now() - tdelta
sources_seen = {}
event_dict = {}
nodes = []
for res in session.read('/noderange/{0}/nodes/'.format(args[0])):
node = res.get('item', {}).get('href', '/').replace('/', '')
nodes.append(node)
event_dict[node] = []
for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
if 'error' in rsp:
sys.stderr.write(rsp['error'] + '\n')
@@ -164,71 +96,6 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
sys.stderr.write('{0}: {1}\n'.format(node, thisdata['error']))
exitcode |= 1
if 'events' in thisdata:
evtdata = thisdata['events']
for evt in evtdata:
if evt.get('log_id', None):
sources_seen.setdefault(node, set()).add(evt['log_id'])
if listmode:
continue
if wantsources is not None:
# Filter before any tail is taken, or asking for the last
# few entries of one log would answer with fewer
evtdata = [x for x in evtdata if (x.get('log_id', '')
or '').lower()
in wantsources]
if options.lines:
event_dict[node].extend(evtdata)
else:
for evt in evtdata:
if options.timeframe:
# check if line is in timeframe
if 'timestamp' in evt and evt['timestamp'] is not None:
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
if display > timeframe:
print('{0}: {1}'.format(node, format_event(evt)))
else:
print('{0}: {1}'.format(node, format_event(evt)))
if listmode:
for node in nodes:
found = sorted(sources_seen.get(node, ()))
if found:
print('{0}: {1}'.format(node, ','.join(found)))
else:
sys.stderr.write(
'No event log sources reported for "{0}"\n'.format(node))
elif wantsources is not None:
for node in nodes:
found = set(x.lower() for x in sources_seen.get(node, ()))
missing = wantsources - found
if not missing:
continue
if found:
sys.stderr.write('{0}: no such log source: {1}, this node has {2}\n'
.format(node, ','.join(sorted(missing)),
','.join(sorted(sources_seen[node]))))
else:
# A log that holds no entries names no source, so an empty answer
# here is as often a quiet platform as a wrong name. Either way
# saying nothing would look like the log simply had nothing in it.
sys.stderr.write(
'{0}: no entries from any log source, so nothing could match '
'{1}\n'.format(node, ','.join(sorted(missing))))
exitcode |= 1
if options.lines and not listmode:
for node in nodes:
evtdata_list = event_dict[node]
if len(evtdata_list) != 0:
if len(evtdata_list) > options.lines:
evtdata_list = evtdata_list[-abs(options.lines):]
for evt in evtdata_list:
if options.timeframe:
if 'timestamp' in evt and evt['timestamp'] is not None:
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
if display > timeframe:
print('{0}: {1}'.format(node, format_event(evt)))
else:
print('{0}: {1}'.format(node, format_event(evt)))
sys.exit(exitcode)
evtdata = thisdata['events']
for evt in evtdata:
print '{0}: {1}'.format(node, format_event(evt))
+12 -88
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -56,35 +56,19 @@ components = ['all']
argparser = optparse.OptionParser(
usage="Usage: "
"%prog <noderange> [list][updatestatus][updatetypes][update [--backup] "
"[--parameterfile <file>] <file>]|[<components>]")
"%prog <noderange> [list][update [--backup <file>]]|[<components>]")
argparser.add_option('-b', '--backup', action='store_true',
help='Target a backup bank rather than primary')
argparser.add_option('-p', '--parameterfile', type='string',
help='When updating, use the specified parameter file; see '
'nodefirmware(8), some platforms need it to say what '
'kind of firmware the image holds')
argparser.add_option('-m', '--maxnodes', type='int',
help='When updating, prompt if more than the specified '
'number of servers will be affected')
(options, args) = argparser.parse_args()
upfile = None
querystatus = False
querytypes = False
try:
noderange = args[0]
if len(args) > 1:
if args[1] == 'update':
upfile = args[2]
else:
comps = []
if args[1] == 'list':
comps = args[2:]
elif args[1] == 'updatestatus':
querystatus = True
elif args[1] == 'updatetypes':
querytypes = True
else:
comps = args[1:]
components = []
@@ -100,9 +84,9 @@ client.check_globbing(noderange)
def get_update_progress(session, url):
for res in session.read(url):
status = res.get('phase', 'error')
percent = res.get('progress', None)
detail = res.get('detail', repr(res))
status = res['phase']
percent = res['progress']
detail = res['detail']
if status == 'error':
text = 'error!'
else:
@@ -111,7 +95,6 @@ def get_update_progress(session, url):
def update_firmware(session, filename):
global exitcode
session.stop_if_noderange_over(noderange, options.maxnodes)
output = sq.ScreenPrinter(noderange, session)
nodeurls = {}
filename = os.path.abspath(filename)
@@ -120,37 +103,9 @@ def update_firmware(session, filename):
upargs = {'filename': filename}
if options.backup:
upargs['bank'] = 'backup'
if options.parameterfile:
with open(options.parameterfile, 'rb') as pf:
pfdata = pf.read()
upargs['parameterdata'] = pfdata
noderrs = {}
if session.unixdomain:
filesbynode = {}
for exp in session.create('/noderange/{0}/attributes/expression'.format(noderange),
{'expression': filename}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
filesbynode[node] = ex[node]['value']
if not isinstance(filesbynode[node], bytes) and not isinstance(filesbynode[node], str):
filesbynode[node] = filesbynode[node].encode('utf-8')
for node in filesbynode:
endfilename = filesbynode[node]
of = open(endfilename, 'rb')
try:
session.add_file(endfilename, of.fileno(), 'rb')
except Exception:
pass
for res in session.create(resource, upargs):
if 'created' not in res:
if not res.get('databynode', None):
# A failure that is not attributed to any node still has to be
# reported, or the command looks like it quietly did nothing
exitcode |= client.printerror(res)
continue
for nodename in res.get('databynode', ()):
output.set_output(nodename, 'error!')
noderrs[nodename] = res['databynode'][nodename].get(
@@ -181,62 +136,31 @@ def update_firmware(session, filename):
def show_firmware(session):
global exitcode
firmware_shown = False
nodes_matched = False
for component in components:
category = 'all'
if component in ('adapters', 'disks', 'misc', 'core'):
category = component
component = 'all'
for res in session.read(
'/noderange/{0}/inventory/firmware/{2}/{1}'.format(
noderange, component, category)):
nodes_matched = True
'/noderange/{0}/inventory/firmware/all/{1}'.format(
noderange, component)):
exitcode |= client.printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
exitcode |= client.printerror(res['databynode'][node], node)
if 'firmware' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['firmware']:
for prefix in inv:
firmware_shown = True
printfirm(node, prefix, inv[prefix])
if not nodes_matched:
sys.stderr.write('No matching nodes for noderange "{0}"\n'.format(noderange))
elif not firmware_shown and not exitcode:
# Asking about firmware the target does not describe is a legitimate
# question with an empty answer, not a mistake in how it was asked
sys.stderr.write('No firmware reported for "{0}"\n'.format(
','.join(components)))
def show_update_types(session):
global exitcode
for res in session.read(
'/noderange/{0}/inventory/firmware/updatetypes'.format(noderange)):
exitcode |= client.printerror(res)
for node in res.get('databynode', {}):
types = res['databynode'][node].get('types', None)
if types:
print('{0}: {1}'.format(node, ','.join(types)))
if not firmware_shown:
argparser.print_help()
try:
session = client.Command()
if querystatus:
for res in session.read(
'/noderange/{0}/inventory/firmware/updatestatus'.format(noderange)):
exitcode |= client.printerror(res)
for node in res.get('databynode', {}):
currstat = res['databynode'][node].get('status', None)
if currstat:
print('{}: {}'.format(node, currstat))
elif querytypes:
show_update_types(session)
elif upfile is None:
if upfile is None:
show_firmware(session)
else:
update_firmware(session, upfile)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
sys.exit(exitcode)
+6 -8
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -42,7 +42,7 @@ argparser = optparse.OptionParser(
\n %prog [options] nodegroup nodes=value1,value2
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes are inherited')
help='Show information about how attributes inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
@@ -60,12 +60,10 @@ nodetype="nodegroups"
try:
nodegroups = args[0]
client.check_globbing(nodegroups)
nodelist = '/{0}/{1}/'.format(nodetype,nodegroups)
except IndexError:
argparser.print_help()
sys.exit(1)
nodelist = '/nodegroups/'
client.check_globbing(nodegroups)
session = client.Command()
exitcode = 0
@@ -99,7 +97,7 @@ if len(args) > 1:
requestargs=args[1:]
except Exception as e:
print(str(e))
print str(e)
if exitcode != 0:
sys.exit(exitcode)
@@ -125,6 +123,6 @@ else:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print(res['item']['href'].replace('/', ''))
print res['item']['href'].replace('/', '')
sys.exit(exitcode)
sys.exit(exitcode)
+12 -13
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -45,15 +45,14 @@ except IndexError:
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
for group in noderange.split(','):
attribs = {'name': group}
for arg in args[1:]:
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/nodegroups/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/nodegroups/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
+8 -9
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -43,11 +43,10 @@ noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
for group in noderange.split(','):
for r in session.delete('/nodegroups/{0}'.format(group)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
for r in session.delete('/nodegroups/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
+6 -8
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,8 +32,7 @@ if path.startswith('/opt'):
import confluent.client as client
if sys.version_info[0] < 3:
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
@@ -50,7 +49,6 @@ codemappings = {
'warning': 1,
'critical': 2,
'failed': 2,
'unknown': 2,
}
def main():
@@ -84,13 +82,13 @@ def main():
healthexplanations[node] = []
for sensor in health[node]['sensors']:
explanation = sensor['name'] + ':'
if sensor.get('value', None) is not None:
if sensor['value'] is not None:
explanation += str(sensor['value'])
if sensor.get('units', None) is not None:
if sensor['units'] is not None:
explanation += sensor['units']
if sensor.get('states', None):
if sensor['states']:
explanation += ','
if sensor.get('states', None):
if sensor['states']:
explanation += ','.join(sensor['states'])
healthexplanations[node].append(explanation)
if node in healthbynode and node in healthexplanations:
+5 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -31,7 +31,7 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off|blink]")
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off]")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -42,6 +42,9 @@ client.check_globbing(noderange)
identifystate = None
if len(sys.argv) > 2:
identifystate = sys.argv[2]
else:
argparser.print_help()
sys.exit(1)
session = client.Command()
exitcode = 0
sys.exit(
+19 -71
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -34,12 +34,9 @@ if path.startswith('/opt'):
import confluent.client as client
if sys.version_info[0] < 3:
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
filters = []
wanted = []
matched = False
def pretty(text):
@@ -51,43 +48,28 @@ def pretty(text):
def print_mem_info(node, prefix, meminfo):
memdescfmt = '{0}GB PC'
if meminfo['memory_type'] is None:
memdescfmt = '{0}GB '
elif meminfo['memory_type'] == 'DDR3 SDRAM':
if meminfo['memory_type'] == 'DDR3 SDRAM':
memdescfmt += '3-{1} '
elif 'DDR4' in meminfo['memory_type']:
elif meminfo['memory_type'] == 'DDR4 SDRAM':
memdescfmt += '4-{1} '
elif 'DDR5' in meminfo['memory_type']:
memdescfmt += '5-{1} '
elif 'DCPMM' in meminfo['memory_type']:
memdescfmt = '{0}GB {1} '
meminfo['module_type'] = 'DCPMM'
elif meminfo['memory_type'] == 'HBM':
memdescfmt = '{0}GB HBM '
else:
print('{0}: {1}: Unrecognized Memory'.format(node, prefix))
return
if meminfo.get('ecc', False):
if meminfo['ecc']:
memdescfmt += 'ECC '
modtype = meminfo.get('module_type', None)
if modtype:
memdescfmt += modtype
if meminfo.get('capacity_mb', None):
capacity = meminfo['capacity_mb'] // 1024
memdesc = memdescfmt.format(capacity, meminfo['speed'])
else:
memdesc = 'Unspecified Module'
capacity = meminfo['capacity_mb'] / 1024
memdescfmt += meminfo['module_type']
memdesc = memdescfmt.format(capacity, meminfo['speed'])
print('{0}: {1} description: {2}'.format(node, prefix, memdesc))
print('{0}: {1} manufacturer: {2}'.format(
node, prefix, meminfo['manufacturer']))
print('{0}: {1} model: {2}'.format(node, prefix, meminfo['model']))
print('{0}: {1} serial number: {2}'.format(node, prefix,
meminfo['serial']))
if 'manufacture_date' in meminfo:
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
meminfo['manufacture_date']))
print('{0}: {1} manufacture location: {2}'.format(
node, prefix, meminfo['manufacture_location']))
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
meminfo['manufacture_date']))
print('{0}: {1} manufacture location: {2}'.format(
node, prefix, meminfo['manufacture_location']))
exitcode = 0
@@ -110,7 +92,6 @@ usedprefixes = set([])
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [serial|model|uuid|mac]")
argparser.add_option('-j', '--json', action='store_true', help='Output JSON')
argparser.add_option('-s', '--store', action='store_true', help='Store serial, model, and uuid into id.serial, id.model, and id.uuid')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -123,10 +104,8 @@ if len(args) > 1:
os.execlp('nodefirmware', 'nodefirmware', noderange)
else:
url = '/noderange/{0}/inventory/hardware/all/system'
for rawarg in args:
for arg in rawarg.split(','):
if arg in ('serial', 'model', 'uuid', 'mac'):
wanted.append(arg)
for arg in args:
for arg in arg.split(','):
if arg == 'serial':
filters.append(re.compile('serial number'))
elif arg == 'model':
@@ -140,36 +119,17 @@ if len(args) > 1:
try:
if options.json:
databynode = {}
if options.store and len(args) <= 1:
url = '/noderange/{0}/inventory/hardware/all/system'
pushattribs = {}
session = client.Command()
for res in session.read(url.format(noderange)):
printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
if options.store and node not in pushattribs:
pushattribs[node] = {}
printerror(res['databynode'][node], node)
if 'inventory' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['inventory']:
prefix = inv['name']
if options.store and prefix == 'System':
currinfo = inv.get('information', {})
curruuid = currinfo.get('UUID', '')
if curruuid:
curruuid = curruuid.lower()
pushattribs[node]['id.uuid'] = curruuid
currserial = currinfo.get('Serial Number', '')
if currserial:
currserial = currserial.strip()
pushattribs[node]['id.serial'] = currserial
currmodelnum = currinfo.get('Model', '')
if currmodelnum:
currmodelnum = currmodelnum.strip()
pushattribs[node]['id.model'] = currmodelnum
idx = 2
while (node, prefix) in usedprefixes:
prefix = '{0} {1}'.format(inv['name'], idx)
@@ -181,8 +141,8 @@ try:
if node not in databynode:
databynode[node] = {}
databynode[node][prefix] = inv
elif not options.store:
print('{0}: {1}: Not Present'.format(node, prefix))
else:
print '{0}: {1}: Not Present'.format(node, prefix)
continue
info = inv['information']
info.pop('board_extra', None)
@@ -207,29 +167,17 @@ try:
continue
if info[datum] is None:
continue
matched = True
if options.json:
if node not in databynode:
databynode[node] = {}
databynode[node][prefix] = inv
break
elif not options.store:
print(u'{0}: {1} {2}: {3}'.format(node, prefix,
pretty(datum),
info[datum]))
if filters and not matched and not options.store:
# An inventory that does not describe what was asked for is a valid
# answer, but saying nothing at all leaves the caller guessing
sys.stderr.write('No {0} reported for "{1}"\n'.format(
'/'.join(wanted) if wanted else 'matching inventory', noderange))
print(u'{0}: {1} {2}: {3}'.format(node, prefix,
pretty(datum),
info[datum]))
if options.json:
print(json.dumps(databynode, sort_keys=True, indent=4,
separators=(',', ': ')))
if pushattribs:
for node in pushattribs:
for rsp in session.update('/nodes/{0}/attributes/current'.format(node), pushattribs[node]):
if 'error' in rsp:
sys.stderr.write(rsp['error'] + '\n')
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
-176
View File
@@ -1,176 +0,0 @@
#!/usr/libexec/platform-python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# 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.
__author__ = 'tkucherera'
import optparse
import os
import signal
import sys
import subprocess
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog <start_node> -i <interface> <end_node> -e <eface>",
)
argparser.add_option('-i', '--interface', type='str',
help='interface to check path against for the start node')
argparser.add_option('-e', '--eface', type='str',
help='interface to check path against for the end node')
argparser.add_option('-c', '--cumulus', action="store_true", dest="cumulus",
help='return layer 2 route through cumulus switches only')
(options, args) = argparser.parse_args()
try:
start_node = args[0]
end_node = args[1]
interface = options.interface
eface = options.eface
except IndexError:
argparser.print_help()
sys.exit(1)
session = client.Command()
def get_neighbors(switch):
switch_neigbors = []
url = '/networking/neighbors/by-switch/{0}/by-peername/'.format(switch)
for neighbor in session.read(url):
try:
switch = neighbor['item']['href'].strip('/')
except:
continue
if switch in all_switches:
switch_neigbors.append(switch)
return switch_neigbors
def find_path(start, end, path=[]):
path = path + [start]
if start == end:
return path # If start and end are the same, return the path
for node in get_neighbors(start):
if node not in path:
new_path = find_path(node, end, path)
if new_path:
return new_path # If a path is found, return it
return None # If no path is found, return None
def is_cumulus(switch):
try:
read_attrib = subprocess.check_output(['nodeattrib', switch, 'hardwaremanagement.method'])
except subprocess.CalledProcessError:
return False
for attribs in read_attrib.decode('utf-8').split('\n'):
if len(attribs.split(':')) > 1:
attrib = attribs.split(':')
if attrib[2].strip() == 'affluent':
return True
else:
return False
else:
return False
def host_to_switch(node, interface=None):
# first check the the node config to see what switches are connected
# if host is in rhel can use nmstate package
if node in all_switches:
return [node]
switches = []
netarg = 'net.*.switch'
if interface:
netarg = 'net.{0}.switch'.format(interface)
try:
read_attrib = subprocess.check_output(['nodeattrib', node, netarg])
except subprocess.CalledProcessError:
return False
for attribs in read_attrib.decode('utf-8').split('\n'):
attrib = attribs.split(':')
try:
if ' net.mgt.switch' in attrib or attrib[2] == '':
continue
except IndexError:
continue
switch = attrib[2].strip()
if is_cumulus(switch) and options.cumulus:
switches.append(switch)
else:
switches.append(switch)
return switches
def path_between_nodes(start_switches, end_switches):
for start_switch in start_switches:
for end_switch in end_switches:
if start_switch == end_switch:
return [start_switch]
else:
path = find_path(start_switch, end_switch)
if path:
return path
else:
return 'No path found'
all_switches = []
for res in session.read('/networking/neighbors/by-switch/'):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
switch = (res['item']['href'].replace('/', ''))
all_switches.append(switch)
end_nodeslist = []
nodelist = '/noderange/{0}/nodes/'.format(end_node)
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
elem=(res['item']['href'].replace('/', ''))
end_nodeslist.append(elem)
start_switches = host_to_switch(start_node, interface)
for end_node in end_nodeslist:
if end_node:
end_switches = host_to_switch(end_node, eface)
if not end_switches:
print('Error: net.{0}.switch attribute is not valid'.format(eface))
continue
path = path_between_nodes(start_switches, end_switches)
print(f'{start_node} to {end_node}: {path}')
# TODO dont put switches that are connected through management interfaces.
+14 -95
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
@@ -19,6 +19,7 @@ import optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
@@ -35,131 +36,49 @@ exitcode = 0
argparser = optparse.OptionParser(
usage="Usage: "
"%prog <noderange> [list][install <license_filename>|save <directory>|delete <license_feature_name>]")
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to delete licenses from, '
'prompting if over the threshold')
"%prog <noderange> [list][install <file>]")
(options, args) = argparser.parse_args()
upfile = None
downdir = None
delete = False
try:
noderange = args[0]
if len(args) > 1:
if args[1] == 'install':
upfile = args[2]
elif args[1] == 'save':
downdir = args[2]
elif args[1] == 'delete':
delete = args[2]
elif args[1] != 'list':
argparser.print_help()
sys.exit(1)
else:
components = ['all']
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
def install_license(session, filename):
global exitcode
resource = '/noderange/{0}/configuration/' \
'management_controller/licenses/'.format(noderange)
filename = os.path.abspath(filename)
instargs = {'filename': filename}
if session.unixdomain:
filesbynode = {}
for exp in session.create('/noderange/{0}/attributes/expression'.format(noderange),
{'expression': filename}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
filesbynode[node] = ex[node]['value']
if not isinstance(filesbynode[node], bytes) and not isinstance(filesbynode[node], str):
filesbynode[node] = filesbynode[node].encode('utf-8')
for node in filesbynode:
endfilename = filesbynode[node]
of = open(endfilename, 'rb')
try:
session.add_file(endfilename, of.fileno(), 'rb')
except Exception:
pass
for res in session.create(resource, instargs):
for node in res.get('databynode', []):
if 'error' in res['databynode'][node]:
sys.stderr.write('{0}: {1}\n'.format(
node, res['databynode'][node]['error']))
sys.exit(res['databynode'][node].get('errorcode', 1))
show_licenses(session)
def save_licenses(session, dirname):
global exitcode
resource = '/noderange/{0}/configuration/' \
'management_controller/save_licenses'.format(noderange)
filename = os.path.abspath(dirname)
instargs = {'dirname': filename}
for res in session.create(resource, instargs):
for node in res.get('databynode', {}):
fname = res['databynode'][node].get('filename', None)
if fname:
print('{0}: Saved license to {1}'.format(node, fname))
elif 'error' in res['databynode'][node]:
sys.stderr.write('{0}: {1}\n'.format(node, res['databynode'][node]['error']))
else:
sys.stderr.write('{0}: {1}\n'.format(node, repr(res['databynode'][node])))
pass # print(repr(res))
show_licenses()
def show_licenses(session):
global exitcode
firmware_shown = False
for res in session.read(
'/noderange/{0}/configuration/management_controller/licenses/'
'all'.format(noderange)):
exitcode |= client.printerror(res)
for node in res.get('databynode', {}):
for license in res['databynode'][node].get('License', []):
msg = '{0}: {1}'.format(node, license.get('feature',
'Unknown'))
if license.get('state', 'Active') != 'Active':
msg += ' ({0})'.format(license['state'])
print(msg)
print('{0}: {1}'.format(node, license.get('feature',
'Unknown')))
def delete_license(session, licname):
global exitcode
licstodel = []
for res in list(session.read(
'/noderange/{0}/configuration/management_controller/licenses/'
'all'.format(noderange))):
for node in res.get('databynode', {}):
for license in res['databynode'][node].get('License', []):
if license.get('feature', None) == licname:
prefix = '/nodes/{0}/configuration/management_controller/licenses/'.format(node)
for currlic in list(session.read(prefix)):
currlic = currlic.get('item', {}).get('href', 'all')
if currlic == 'all':
continue
currname = list(session.read(prefix + currlic))[0]
currname = currname.get('License', [{}])[0].get('feature', None)
if currname == licname:
list(session.delete(prefix + currlic))
show_licenses(session)
try:
session = client.Command()
if upfile:
install_license(session, upfile)
elif downdir:
save_licenses(session, downdir)
elif delete:
session.stop_if_noderange_over(noderange, options.maxnodes)
delete_license(session, delete)
else:
if upfile is None:
show_licenses(session)
else:
install_license(session, upfile)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
sys.exit(exitcode)
+9 -17
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -15,15 +15,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = 'jjohnson2,alin37,wgrzeda'
__author__ = 'jjohnson2,alin37'
import optparse
import os
import signal
import sys
import asyncio
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
@@ -34,20 +31,17 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.asynclient as client
import confluent.client as client
async def main():
def main():
argparser = optparse.OptionParser(
usage="Usage: %prog noderange\n"
" or: %prog [options] noderange <nodeattribute>...")
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes are inherited')
argparser.add_option('-d', '--delim', metavar="STRING", default = "\n",
help='Delimiter separating the values')
help='Show information about how attributes inherited')
(options, args) = argparser.parse_args()
noderange=""
nodelist=""
list = []
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
@@ -60,18 +54,16 @@ async def main():
requestargs=args[1:]
nodetype='noderange'
if len(args) > 1:
exitcode=await client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
exitcode=client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
else:
async for res in session.read(nodelist):
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
elem=(res['item']['href'].replace('/', ''))
list.append(elem)
print(options.delim.join(list))
print res['item']['href'].replace('/', '')
sys.exit(exitcode)
if __name__ == '__main__':
asyncio.run(main())
main()
+7 -31
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
@@ -83,8 +83,7 @@ def list_media(noderange, media):
for node in res.get('databynode', []):
url = res['databynode'][node].get('url', None)
name = res['databynode'][node].get('name', None)
if (url and not url.startswith('file:') and
not res['databynode'][node].get('secure', False)):
if url and not res['databynode'][node].get('secure', False):
name += ' (insecure)'
if not name:
continue
@@ -101,37 +100,16 @@ def detach_media(noderange, media):
def upload_media(noderange, media):
global exitcode
if not os.path.exists(media):
sys.stderr.write('Unable to locate requested file {0}\n'.format(
media))
sys.exit(404)
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
filename = os.path.abspath(media)
resource = '/noderange/{0}/media/uploads/'.format(noderange)
filename = os.path.abspath(filename)
upargs = {'filename': filename}
noderrs = {}
if session.unixdomain:
filesbynode = {}
for exp in session.create('/noderange/{0}/attributes/expression'.format(noderange),
{'expression': filename}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
filesbynode[node] = ex[node]['value']
if not isinstance(filesbynode[node], bytes) and not isinstance(filesbynode[node], str):
filesbynode[node] = filesbynode[node].encode('utf-8')
for node in filesbynode:
endfilename = filesbynode[node]
if not os.path.exists(endfilename):
sys.stderr.write('Unable to locate requested file {0}\n'.format(
endfilename))
sys.exit(404)
of = open(endfilename, 'rb')
try:
session.add_file(endfilename, of.fileno(), 'rb')
except Exception:
pass
nodeurls = {}
for res in session.create(resource, upargs):
if 'created' not in res:
@@ -208,7 +186,5 @@ def main():
argparser.print_help()
sys.exit(1)
handler(noderange, media)
if __name__ == '__main__':
main()
sys.exit(exitcode)
main()
-157
View File
@@ -1,157 +0,0 @@
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# 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.
from collections import deque
import optparse
import os
import select
import signal
import subprocess
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
argparser.add_option('-o', '--origname', action='store_true',
help='Use original nodename in print out even if substituted')
argparser.add_option('-s', '--substitutename',
help='Use a different name other than the nodename for ping, with {}, it is the entire name evaluated as an expression, otherwise it is used as a suffix')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
(options, args) = argparser.parse_args()
if len(args) < 1:
argparser.print_help()
sys.exit(1)
client.check_globbing(args[0])
concurrentprocs = options.count
c = client.Command()
currprocs = 0
all = set([])
poller = select.epoll()
pipedesc = {}
pendingexecs = deque()
exitcode = 0
nodemap = {}
if options.substitutename:
subname = options.substitutename
if '{' not in subname:
subname = '{node}' + subname
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': subname}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
nodemap[node] = ex[node]['value']
for rsp in c.read('/noderange/{0}/nodes/'.format(args[0])):
if 'error' in rsp:
sys.stderr.write(rsp['error'] + '\n')
exitcode |= rsp.get('errorcode', 1)
if rsp.get('item', {}).get('href', None):
node = rsp['item']['href'][:-1]
pingnode = nodemap.get(node, node)
cmdv = ['ping', '-c', '1', '-W', '1', pingnode]
if currprocs < concurrentprocs:
currprocs += 1
if options.origname:
run_cmdv(node, cmdv, all, poller, pipedesc)
else:
run_cmdv(pingnode, cmdv, all, poller, pipedesc)
else:
if options.origname:
pendingexecs.append((node, cmdv))
else:
pendingexecs.append((pingnode, cmdv))
if not all or exitcode:
sys.exit(exitcode)
rdy = poller.poll(10)
while all:
pernodeout = {}
for r in rdy:
r = r[0]
desc = pipedesc[r]
r = desc['file']
node = desc['node']
data = True
singlepoller = select.epoll()
singlepoller.register(r, select.EPOLLIN)
while data and singlepoller.poll(0):
data = r.readline()
if not data:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
poller.unregister(r)
r.close()
if desc['type'] == 'stdout':
if ret:
print('{0}: no_ping'.format(node))
else:
print('{0}: ping'.format(node))
if pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, poller, pipedesc)
singlepoller.close()
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
if options.nonodeprefix:
sys.stdout.write(line)
else:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy = poller.poll(10)
sys.exit(exitcode)
def run_cmdv(node, cmdv, all, poller, pipedesc):
nopen = subprocess.Popen(
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pipedesc[nopen.stdout.fileno()] = {'node': node, 'popen': nopen,
'type': 'stdout', 'file': nopen.stdout}
pipedesc[nopen.stderr.fileno()] = {'node': node, 'popen': nopen,
'type': 'stderr', 'file': nopen.stderr}
all.add(nopen.stdout)
poller.register(nopen.stdout, select.EPOLLIN)
all.add(nopen.stderr)
poller.register(nopen.stderr, select.EPOLLIN)
if __name__ == '__main__':
run()
+8 -21
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,16 +32,11 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog [options] <noderange> "
"([status|on|off|shutdown|boot|reset|pdu_status|pdu_off|pdu_on])")
usage="Usage: %prog [options] noderange "
"([status|on|off|shutdown|boot|reset])")
argparser.add_option('-p', '--showprevious', dest='previous',
action='store_true', default=False,
help='Show previous power state')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to change power state, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -51,27 +46,22 @@ except IndexError:
client.check_globbing(noderange)
setstate = None
if len(args) > 1:
setstate = args[1]
if setstate == 'softoff':
setstate = 'shutdown'
elif not args[1] in ('stat', 'state', 'status'):
setstate = args[1]
if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset', 'pdu_status', 'pdu_stat', 'pdu_on', 'pdu_off', 'status', 'stat', 'state'):
if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'):
argparser.print_help()
sys.exit(1)
session = client.Command()
exitcode = 0
session.add_precede_key('oldstate')
powurl = 'state'
if setstate and setstate.startswith('pdu_'):
setstate = setstate.replace('pdu_', '')
powurl = 'inlets/all'
if setstate in ('status', 'state', 'stat'):
setstate = None
if options.previous:
# get previous states
prev = {}
for rsp in session.read("/noderange/{0}/power/{1}".format(noderange, powurl)):
for rsp in session.read("/noderange/{0}/power/state".format(noderange)):
# gets previous (current) states
databynode = rsp["databynode"]
@@ -82,7 +72,4 @@ if options.previous:
# add dictionary to session
session.add_precede_dict(prev)
def outhandler(node, res):
for k in res[node]:
client.cprint('{0}: {1}: {2}'.format(node, k.replace('inlet_', ''), res[node][k]))
sys.exit(session.simple_noderange_command(noderange, '/power/{0}'.format(powurl), setstate, promptover=options.maxnodes, key='state', outhandler=outhandler))
sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate))
+18 -30
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -15,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import optparse
import os
import signal
@@ -33,32 +32,21 @@ if path.startswith('/opt'):
import confluent.client as client
async def main():
argparser = optparse.OptionParser(
usage='''\n %prog <noderange>
argparser = optparse.OptionParser(
usage='''\n %prog noderange
\n ''')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to delete, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
session.stop_if_noderange_over(noderange, options.maxnodes)
for r in session.delete('/noderange/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
if __name__ == '__main__':
asyncio.run(main())
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
for r in session.delete('/noderange/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
+3 -7
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,10 +32,6 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to reseat, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -47,7 +43,7 @@ session = client.Command()
exitcode = 0
errorNodes = set([])
session.stop_if_noderange_over(noderange, options.maxnodes)
success = session.simple_noderange_command(noderange, 'power/reseat', 'reseat', key='reseat', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
@@ -60,7 +56,7 @@ for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
goodNodes = allNodes - errorNodes
for node in goodNodes:
print(node + ": Reseat successful")
print node + ": Reseat successful"
sys.exit(success)
+10 -32
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -35,20 +35,13 @@ if path.startswith('/opt'):
import confluent.client as client
import confluent.screensqueeze as sq
import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog <file/directorylist> <noderange>:<destination>",
usage="Usage: %prog location noderange:location",
)
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run rsync to, '
'prompting if over the threshold')
argparser.add_option('-l', '--loginname', type='str',
help='Username to use when connecting, defaults to current user.')
argparser.add_option('-s', '--substitutename',
help='Use a different name other than the nodename for rsync')
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of nodes to concurrently rsync')
# among other things, FD_SETSIZE limits. Besides, spawning too many
@@ -62,39 +55,24 @@ def run():
noderange, targpath = args[-1].split(':', 1)
client.check_globbing(noderange)
c = client.Command()
cmdstr = ' '.join(args[:-1])
cmdstr = " ".join(args[:-1])
cmdstr = 'rsync -av --info=progress2 ' + cmdstr
targname = options.substitutename
if targname and '{' in targname:
targname = targname + ':'
elif targname:
targname = '{node}' + targname + ':'
else:
targname = '{node}:'
if options.loginname:
cmdstr += ' {}@'.format(options.loginname) + targname + targpath
else:
cmdstr += ' {}'.format(targname) + targpath
cmdstr += ' {node}:' + targpath
currprocs = 0
all = set([])
pipedesc = {}
pendingexecs = deque()
exitcode = 0
c.stop_if_noderange_over(noderange, options.maxnodes)
for exp in c.create('/noderange/{0}/attributes/expression'.format(noderange),
{'expression': cmdstr}):
{'expression': cmdstr}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value']
if not isinstance(cmd, bytes) and not isinstance(cmd, str):
cmd = cmd.encode('utf-8')
cmd = ex[node]['value'].encode('utf-8')
cmdv = shlex.split(cmd)
if currprocs < concurrentprocs:
currprocs += 1
@@ -120,7 +98,7 @@ def run():
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = ''
pernodeout[node] += client.stringify(data)
pernodeout[node] += data
if '\n' in pernodeout[node]:
currout, pernodeout[node] = pernodeout[node].split('\n', 1)
if currout:
@@ -139,7 +117,7 @@ def run():
output.set_output(node, 'error!')
if node not in nodeerrs:
nodeerrs[node] = ''
nodeerrs[node] += client.stringify(data)
nodeerrs[node] += data
else:
pop = desc['popen']
ret = pop.poll()
+17 -42
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -35,24 +35,17 @@ if path.startswith('/opt'):
import confluent.client as client
import confluent.sortutil as sortutil
devnull = None
def run():
global devnull
devnull = open(os.devnull, 'rb')
argparser = optparse.OptionParser(
usage="Usage: %prog [options] <noderange> <command expression>",
usage="Usage: %prog [options] noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
argparser.add_option('-n', '--nonodeprefix', action='store_true',
help='Do not prefix output with node names')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run the command with, '
'prompting if over the threshold')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
@@ -67,11 +60,10 @@ def run():
currprocs = 0
all = set([])
poller = select.epoll()
pipedesc = {}
pendingexecs = deque()
exitcode = 0
c.stop_if_noderange_over(args[0], options.maxnodes)
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
if 'error' in exp:
@@ -79,29 +71,23 @@ def run():
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value']
if not isinstance(cmd, bytes) and not isinstance(cmd, str):
cmd = cmd.encode('utf-8')
cmd = ex[node]['value'].encode('utf-8')
cmdv = shlex.split(cmd)
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, poller, pipedesc)
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
if not all or exitcode:
sys.exit(exitcode)
rdy = poller.poll(10)
rdy, _, _ = select.select(all, [], [], 10)
while all:
pernodeout = {}
for r in rdy:
r = r[0]
desc = pipedesc[r]
r = desc['file']
node = desc['node']
data = True
singlepoller = select.epoll()
singlepoller.register(r, select.EPOLLIN)
while data and singlepoller.poll(0):
while data and select.select([r], [], [], 0)[0]:
data = r.readline()
if data:
if desc['type'] == 'stdout':
@@ -109,7 +95,6 @@ def run():
pernodeout[node] = []
pernodeout[node].append(data)
else:
data = client.stringify(data)
if options.nonodeprefix:
sys.stderr.write(data)
else:
@@ -121,42 +106,32 @@ def run():
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
poller.unregister(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, poller, pipedesc)
singlepoller.close()
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
if options.nonodeprefix:
sys.stdout.write(line)
else:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy = poller.poll(10)
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
def run_cmdv(node, cmdv, all, poller, pipedesc):
try:
nopen = subprocess.Popen(
cmdv, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError as e:
if e.errno == 2:
sys.stderr.write('{0}: Unable to find local executable file "{1}"'.format(node, cmdv[0]))
return
raise
pipedesc[nopen.stdout.fileno()] = {'node': node, 'popen': nopen,
'type': 'stdout', 'file': nopen.stdout}
pipedesc[nopen.stderr.fileno()] = {'node': node, 'popen': nopen,
'type': 'stderr', 'file': nopen.stderr}
def run_cmdv(node, cmdv, all, pipedesc):
nopen = subprocess.Popen(
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pipedesc[nopen.stdout] = {'node': node, 'popen': nopen,
'type': 'stdout'}
pipedesc[nopen.stderr] = {'node': node, 'popen': nopen,
'type': 'stderr'}
all.add(nopen.stdout)
poller.register(nopen.stdout, select.EPOLLIN)
all.add(nopen.stderr)
poller.register(nopen.stderr, select.EPOLLIN)
if __name__ == '__main__':
run()
+18 -53
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,19 +32,6 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
class hybridcsv(csv.excel):
lineterminator = '\n'
def floatformat(num):
fm = u'{:.5f}'.format(num).rstrip('0')
if fm[-1:] == u'.':
return fm + u'0'
return fm
csv.register_dialect('hybrid', hybridcsv)
import confluent.client as client
sensorcollections = {
@@ -67,7 +54,7 @@ argparser.add_option('-n', '--numreadings', type='int',
argparser.add_option('-c', '--csv', action='store_true',
help='Output in CSV format')
argparser.add_option('-s', '--skipnumberless', action='store_true',
help='Do not show non-numeric sensors')
help='Output in CSV format')
(options, args) = argparser.parse_args()
repeatmode = False
if options.interval:
@@ -107,7 +94,7 @@ def sensorpass(showout=True, appendtime=False):
if 'error' in reading:
sys.stderr.write('Error: {0}\n'.format(reading['error']))
if 'errorcode' in reading:
exitcode |= reading['errorcode']
exitcode |= exitcode
else:
exitcode |= 1
if 'databynode' not in reading:
@@ -120,45 +107,36 @@ def sensorpass(showout=True, appendtime=False):
sys.stderr.write(
'{0}: Error: {1}\n'.format(node,
reading[node]['error']))
if 'errorcode' in reading[node]:
exitcode |= reading[node]['errorcode']
else:
exitcode |= 1
if 'sensors' not in reading[node]:
continue
for sensedata in reading[node]['sensors']:
if sensedata.get('value', None) is None and options.skipnumberless:
if sensedata['value'] is None and options.skipnumberless:
continue
for redundant_state in ('Non-Critical', 'Critical'):
try:
if sensedata.get('states', False):
sensedata['states'].remove(redundant_state)
sensedata['states'].remove(redundant_state)
except ValueError:
pass
resultdata[node][sensedata['name']] = sensedata
sensorname = sensedata['name']
sensorheaders[sensorname] = sensorname
if sensedata.get('units', None) not in (None, u''):
if sensedata['units'] not in (None, u''):
sensorheaders[sensorname] += u' ({0})'.format(
sensedata['units'])
if showout:
if sensedata.get('value', None) is None:
if sensedata['value'] is None:
showval = ''
elif isinstance(sensedata['value'], float):
showval = u' {0} '.format(floatformat(sensedata['value']))
showval = u' {0:.5f} '.format(sensedata['value'])
else:
showval = u' {0} '.format(sensedata.get('value', ''))
# A discrete sensor has no reading for a unit to apply
# to, and printing the unit on its own says nothing
if (showval
and sensedata.get('units', None) not in (None, u'')):
showval = u' {0} '.format(sensedata['value'])
if sensedata['units'] not in (None, u''):
showval += sensedata['units']
if sensedata.get('health', 'ok') != 'ok':
if sensedata['health'] != 'ok':
datadescription = [sensedata['health']]
else:
datadescription = []
if sensedata.get('states', False):
datadescription.extend(sensedata['states'])
datadescription.extend(sensedata['states'])
if datadescription:
if showval == '':
showval += u' {0}'.format(
@@ -169,11 +147,8 @@ def sensorpass(showout=True, appendtime=False):
if appendtime:
showval += ' @' + time.strftime(
'%Y-%m-%dT%H:%M:%S')
printval = u'{0}: {1}:{2}'.format(
node, sensedata['name'], showval)
if not isinstance(printval, str):
printval = printval.encode('utf-8')
print(printval)
print(u'{0}: {1}:{2}'.format(
node, sensedata['name'], showval).encode('utf-8'))
sys.stdout.flush()
return resultdata
@@ -181,7 +156,7 @@ def sensorpass(showout=True, appendtime=False):
def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
for nodekey in resdata:
if showtime:
if isinstance(showtime, int):
if showtime.is_integer():
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
else:
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S.') +
@@ -204,13 +179,10 @@ def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
datum = ','.join([datum, healthstates])
else:
datum = healthstates
if isinstance(datum, float):
datum = floatformat(datum)
rowdata.append(datum)
except KeyError:
rowdata.append('N/A')
csvwriter.writerow(rowdata)
sys.stdout.flush()
def main():
@@ -224,14 +196,10 @@ def main():
orderedsensors.append(name)
orderedsensors.sort()
for name in orderedsensors:
headername = sensorheaders[name]
if (not isinstance(headername, str) and
not isinstance(headername, bytes)):
headername = headername.encode('utf-8')
headernames.append(headername)
headernames.append(sensorheaders[name].encode('utf-8'))
if options.csv:
linebyline = False
csvwriter = csv.writer(sys.stdout, dialect='hybrid')
csvwriter = csv.writer(sys.stdout)
if options.interval:
csvwriter.writerow(['time', 'node'] + headernames)
else:
@@ -252,7 +220,7 @@ def main():
sys.exit(exitcode)
sleeptime = nextstart - os.times()[4]
if sleeptime > 0:
time.sleep(sleeptime)
time.sleep(nextstart - os.times()[4])
else:
if options.csv:
format_csv(csvwriter, orderedsensors, resdata, showtime=False)
@@ -265,6 +233,3 @@ try:
except KeyboardInterrupt:
print('')
sys.exit(0)
# Falling off the end exited 0 whatever sensorpass recorded, so only the
# --interval path ever reported a failed read.
sys.exit(exitcode)
+3 -7
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
@@ -32,7 +32,7 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='Usage: %prog [options] <noderange> [default|cd|network|http|setup|hd|usb|floppy]')
usage='Usage: %prog [options] noderange [default|cd|network|setup|hd]')
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
@@ -43,10 +43,7 @@ argparser.add_option('-p', '--persist', dest='persist', action='store_true',
argparser.add_option('-u', '--uefi', dest='uefi', action='store_true',
default=True,
help='Request UEFI style boot (rather than BIOS)')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to modify next boot device, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
try:
@@ -66,7 +63,6 @@ if options.biosmode:
bootmode = 'bios'
else:
bootmode = 'uefi'
session.stop_if_noderange_over(noderange, options.maxnodes)
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist))
+28 -89
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -34,33 +34,18 @@ if path.startswith('/opt'):
import confluent.client as client
import confluent.sortutil as sortutil
devnull = None
def run():
global devnull
devnull = open(os.devnull, 'rb')
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
argparser.add_option('-l', '--loginname', type='str',
help='Username to use when connecting, defaults to current user.')
argparser.add_option('-n', '--nonodeprefix', action='store_true',
help='Do not prefix output with node names')
argparser.add_option('-p', '--port', type='int', default=0,
help='Specify a custom port for ssh')
argparser.add_option('-s', '--substitutename',
help='Use a different name other than the nodename for ssh')
argparser.add_option('-t', '--timeout', type='int', default=0,
help='Timeout in seconds for each node ssh connection')
argparser.add_option('-x', '--noexpression', action='store_true',
help='Suppress expression expansion of command')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run remote ssh command to, '
'prompting if over the threshold')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
@@ -79,70 +64,31 @@ def run():
pendingexecs = deque()
exitcode = 0
c.stop_if_noderange_over(args[0], options.maxnodes)
nodemap = {}
if options.substitutename:
subname = options.substitutename
if '{' not in subname:
subname = '{node}' + subname
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': subname}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
nodemap[node] = ex[node]['value']
cmdparms = []
if options.noexpression:
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
break
node = res['item']['href'][:-1]
cmdparms.append((node, cmdstr))
else:
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmdparms.append((node, ex[node]['value']))
poller = select.epoll()
for node, cmd in cmdparms:
sshnode = nodemap.get(node, node)
if not isinstance(cmd, str) and not isinstance(cmd, bytes):
cmd = cmd.encode('utf-8')
cmdv = ['ssh']
if options.port:
cmdv += ['-p', '{0}'.format(options.port)]
if options.loginname:
cmdv += ['-l', options.loginname]
if options.timeout:
cmdv += ['-o', 'ConnectTimeout={0}'.format(options.timeout)]
cmdv += [sshnode, cmd]
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, poller, pipedesc)
else:
pendingexecs.append((node, cmdv))
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = ['ssh', node, cmd]
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
if not all or exitcode:
sys.exit(exitcode)
rdy = poller.poll(10)
rdy, _, _ = select.select(all, [], [], 10)
while all:
pernodeout = {}
for r in rdy:
r = r[0]
desc = pipedesc[r]
r = desc['file']
node = desc['node']
data = True
singlepoller = select.epoll()
singlepoller.register(r, select.EPOLLIN)
while data and singlepoller.poll(0):
while data and select.select([r], [], [], 0)[0]:
data = r.readline()
if data:
if desc['type'] == 'stdout':
@@ -150,7 +96,6 @@ def run():
pernodeout[node] = []
pernodeout[node].append(data)
else:
data = client.stringify(data)
if options.nonodeprefix:
sys.stderr.write(data)
else:
@@ -162,37 +107,31 @@ def run():
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
poller.unregister(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, poller, pipedesc)
singlepoller.close()
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
if options.nonodeprefix:
sys.stdout.write(line)
else:
line = line.lstrip('\x08')
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy = poller.poll(10)
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
def run_cmdv(node, cmdv, all, poller, pipedesc):
def run_cmdv(node, cmdv, all, pipedesc):
nopen = subprocess.Popen(
cmdv, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pipedesc[nopen.stdout.fileno()] = {'node': node, 'popen': nopen,
'type': 'stdout', 'file': nopen.stdout}
pipedesc[nopen.stderr.fileno()] = {'node': node, 'popen': nopen,
'type': 'stderr', 'file': nopen.stderr}
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pipedesc[nopen.stdout] = {'node': node, 'popen': nopen,
'type': 'stdout'}
pipedesc[nopen.stderr] = {'node': node, 'popen': nopen,
'type': 'stderr'}
all.add(nopen.stdout)
all.add(nopen.stderr)
poller.register(nopen.stdout, select.EPOLLIN)
poller.register(nopen.stderr, select.EPOLLIN)
if __name__ == '__main__':
+6 -53
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
@@ -20,10 +20,6 @@ import os
import signal
import sys
try:
input = raw_input
except NameError:
pass
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
@@ -67,16 +63,10 @@ def _print_cfg(scfg):
sys.stderr.write(e['error'] + '\n')
exitcode = e.get('errorcode', 1)
for node in e.get('databynode', {}):
curr = e['databynode'][node]
if 'error' in curr:
if 'no available drives' in curr['error']:
curr['error'] += ' (drives must be in unconfigured state to be available, they must not be in jbod or online state)'
sys.stderr.write('{0}: {1}\n'.format(node, curr['error']))
exitcode = curr.get('errorcode', 1)
continue
if node not in storagebynode:
storagebynode[node] = {'disks': [], 'arrays': [],
'volumes': []}
curr = e['databynode'][node]
storagebynode[node][curr['type'] + 's'].append(curr)
for node in storagebynode:
for disk in sorted(storagebynode[node]['disks'],
@@ -118,7 +108,6 @@ def createstorage(noderange, options, args):
sys.stderr.write('-r and -d are required arguments to create array\n')
sys.exit(1)
session = client.Command()
session.stop_if_noderange_over(noderange, options.maxnodes)
names = options.name
if names is None:
names = ''.join(args)
@@ -126,8 +115,6 @@ def createstorage(noderange, options, args):
'name': names}
if options.size:
parms['size'] = options.size
if options.stripsizes:
parms['stripsizes'] = options.stripsizes
_print_cfg(session.create(
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
noderange, names), parms))
@@ -143,7 +130,6 @@ def deletestorage(noderange, options, args):
else:
names = options.name
session = client.Command()
session.stop_if_noderange_over(noderange, options.maxnodes)
for rsp in session.delete(
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
noderange, names)):
@@ -161,27 +147,13 @@ def deletestorage(noderange, options, args):
print(repr(rsp))
def setdisk(noderange, options, args):
if options.disks is None:
if len(args):
names = args.pop(0)
else:
sys.stderr.write('-d is required to indicate disk to modify\n')
sys.exit(1)
else:
names = options.disks
if not len(args) or args[0] not in ('hotspare', 'jbod', 'unconfigured'):
sys.stderr.write('diskset requires valid state as argument (hotspare, jbod, unconfigured)\n')
sys.exit(1)
session = client.Command()
session.stop_if_noderange_over(noderange, options.maxnodes)
scfg = session.update('/noderange/{0}/configuration/storage/disks/{1}'.format(noderange, names), {'state': args[0]})
_print_cfg(scfg)
# def setstorage(noderange, options, args):
# pass
funmap = {
'create': createstorage,
'show': showstorage,
'diskset': setdisk,
# 'set': setstorage,
'delete': deletestorage,
'rm': deletestorage,
}
@@ -190,7 +162,7 @@ funmap = {
def main():
argparser = OptParser(
usage='Usage: %prog <noderange> [show|create|delete|diskset] [hotspare|jbod|unconfigured] [options]',
usage='Usage: %prog <noderange> [show|create|delete]',
epilog='',
)
argparser.add_option('-r', '--raidlevel', type='int',
@@ -199,8 +171,6 @@ def main():
help='Comma separated list of disks to use, or the '
'word "rest" to indicate use of all available '
'disks')
argparser.add_option('-i', '--interactive', default=False, action='store_true',
help='Confirm any potentially destructive changes interactively')
argparser.add_option('-s', '--size', type='str',
help='Comma separated list of sizes to use when '
'creating volumes. The sizes may be absolute '
@@ -213,14 +183,6 @@ def main():
'naming volumes, or selecting a volume for '
'delete. Default behavior is to use '
'implementation provided default names.')
argparser.add_option('-z', '--stripsizes', type='str',
help='Comma separated list of stripsizes to use when creating volumes. '
'This value is in kilobytes. The default behavior is to allow the '
'storage controller to decide.')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to configure storage on, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
if len(args) == 1:
args.append('show')
@@ -236,16 +198,7 @@ def main():
except KeyError:
argparser.print_help()
sys.exit(1)
if options.interactive and operation != 'show':
showstorage(noderange, None, None)
sys.stdout.write('The above configuration will be changed by this action, do you wish to continue (y/N): ')
confirm = input()
if confirm and confirm.lower()[0] != 'y':
sys.stdout.write('Aborting\n')
sys.exit(1)
handler(noderange, options, args[2:])
# The handlers record failures in exitcode, so let a caller see them
sys.exit(exitcode)
if __name__ == '__main__':
+5 -19
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
@@ -64,7 +64,7 @@ def printerror(res, node=None):
def download_servicedata(noderange, media, options):
def download_servicedata(noderange, media):
global exitcode
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
@@ -73,14 +73,8 @@ def download_servicedata(noderange, media, options):
upargs = {'filename': filename}
noderrs = {}
nodeurls = {}
session.stop_if_noderange_over(noderange, options.maxnodes)
for res in session.create(resource, upargs):
if 'created' not in res:
if not res.get('databynode', None):
# A failure that is not attributed to any node still has to be
# reported, or the command looks like it quietly did nothing
printerror(res)
continue
for nodename in res.get('databynode', ()):
output.set_output(nodename, 'error!')
noderrs[nodename] = res['databynode'][nodename].get(
@@ -121,16 +115,12 @@ class OptParser(optparse.OptionParser):
def main():
argparser = OptParser(
usage="Usage: %prog <noderange> servicedata "
"<directory or filename>",
"<filename>",
epilog='\nservicedata will save service data to the given '
'directory. It is saved to the location on the relevant '
'management server (the confluent server if running remote, '
'and the collective.manager if in collective)\n'
'\n\nSee `man %prog` for more info.\n')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to download diagnostic data from, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
media = None
try:
@@ -152,10 +142,6 @@ def main():
except KeyError:
argparser.print_help()
sys.exit(1)
handler(noderange, media, options)
# The handlers record failures in exitcode, so let a caller see them
sys.exit(exitcode)
handler(noderange, media)
if __name__ == '__main__':
main()
main()
-203
View File
@@ -1,203 +0,0 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import base64
import csv
import io
import os
import numpy as np
import sys
try:
import sixel
class DumbWriter(sixel.SixelWriter):
def restore_position(self, output):
return
except ImportError:
pass
def iterm_draw(data):
databuf = data.getbuffer()
datalen = len(databuf)
data = base64.b64encode(databuf).decode('utf8')
sys.stdout.write(
'\x1b]1337;File=inline=1;size={}:'.format(datalen))
sys.stdout.write(data)
sys.stdout.write('\a')
sys.stdout.write('\n')
sys.stdout.flush()
def kitty_draw(data):
data = base64.b64encode(data.getbuffer())
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
sys.stdout.write('\x1b_Ga=T,f=100,m={};'.format(m))
sys.stdout.write(chunk.decode('utf8'))
sys.stdout.write('\x1b\\')
sys.stdout.flush()
sys.stdout.write('\n')
def plot(gui, output, plotdata, bins, fmt):
import matplotlib as mpl
if gui and mpl.get_backend() == 'agg':
sys.stderr.write('Error: No GUI backend available and -g specified!\n')
if not gui:
mpl.use('Agg')
import matplotlib.pyplot as plt
n, bins, patches = plt.hist(plotdata, bins)
plt.show()
if not gui:
if output:
tdata = output
else:
tdata = io.BytesIO()
plt.savefig(tdata)
if not gui and not output:
if fmt == 'environment':
fmt = os.environ.get('CONFLUENT_IMAGE_PROTOCOL', 'kitty')
if fmt == 'sixel':
writer = DumbWriter()
writer.draw(tdata)
elif fmt == 'kitty':
kitty_draw(tdata)
elif fmt == 'iterm':
iterm_draw(tdata)
return n, bins
def textplot(plotdata, bins):
n, bins = np.histogram(plotdata, bins)
labels = []
for bin in bins:
labels.append('{0:0.1f}'.format(bin))
width = 80
# Since this will be primarily piped into, hard to get
# terminal width
labelwidth = 0
for lab in labels:
if len(lab) > labelwidth:
labelwidth = len(lab)
width -= (labelwidth) + 1
labelfmt = '{{0:>{0}s}}|'.format(labelwidth)
maxn = 0.0
for lgth in n:
if lgth > maxn:
maxn = float(lgth)
for i in range(len(n)):
print(labelfmt.format(labels[i]) + '=' * int(np.round((n[i]/maxn) * width)))
return n, bins
histogram = False
aparser = argparse.ArgumentParser(description='Quick access to common statistics')
aparser.add_argument('-c', type=int, default=0, help='Column number to analyze (default is last column)')
aparser.add_argument('-d', default=None, help='Value used to separate columns')
aparser.add_argument('-x', default=False, action='store_true', help='Output histogram in graphical format')
aparser.add_argument('-f', default='environment', help='Format for histogram output (sixel/iterm/kitty)')
aparser.add_argument('-s', default=0, help='Number of header lines to skip before processing')
aparser.add_argument('-g', default=False, action='store_true', help='Open histogram in separate graphical window')
aparser.add_argument('-o', default=None, help='Output histogram to the specified filename in PNG format')
aparser.add_argument('-t', default=False, action='store_true', help='Output a histogram in text format')
aparser.add_argument('-v', default=False, action='store_true', help='Attempt to list nodes relevant to each histogram bar (requires -s, -o, or -t)')
aparser.add_argument('-b', type=int, default=10, help='Number of bins to use in histogram (default is 10)')
args = aparser.parse_args(sys.argv[1:])
plotdata = []
headlines = int(args.s)
while headlines >= 0:
data = sys.stdin.readline()
headlines -= 1
if args.d:
delimiter = args.d
else:
if '\t' in data:
delimiter = '\t'
elif ' ' in data:
delimiter = ' '
elif ',' in data:
delimiter = ','
else:
delimiter = ' ' # handle single column
data = list(csv.reader([data], delimiter=delimiter))[0]
nodebydatum = {}
idx = args.c - 1
autoidx = False
while data:
node = None
if ':' in data[0]:
node, data[0] = data[0].split(':', 1)
else:
node = data[0]
if idx == -1 and not autoidx:
while not autoidx:
try:
datum = float(data[idx])
except ValueError:
idx -= 1
continue
except IndexError:
sys.stderr.write('Unable to identify a numerical column\n')
sys.exit(1)
autoidx = True
else:
datum = float(data[idx])
if node:
if datum in nodebydatum:
nodebydatum[datum].add(node)
else:
nodebydatum[datum] = set([node])
plotdata.append(datum)
data = sys.stdin.readline()
data = list(csv.reader([data], delimiter=delimiter))[0]
n = None
if args.g or args.o or args.x:
n, bins = plot(args.g, args.o, plotdata, bins=args.b, fmt=args.f)
if args.t:
n, bins = textplot(plotdata, bins=args.b)
print('Samples: {5} Min: {3} Median: {0} Mean: {1} Max: {4} StandardDeviation: {2} Sum: {6}'.format(np.median(plotdata), np.mean(plotdata), np.std(plotdata), np.min(plotdata), np.max(plotdata), len(plotdata), np.sum(plotdata)))
if args.v and n is not None and nodebydatum:
print('')
currbin = bins[0]
bins = bins[1:]
currbinmembers = []
for datum in sorted(nodebydatum):
if datum > bins[0]:
nextbin = None
endbin = bins[0]
while len(bins) and bins[0] < datum:
nextbin = bins[0]
bins = bins[1:]
if not nextbin:
nextbin = np.max(plotdata)
print('Entries between {0} and {1}'.format(currbin, endbin))
currbin = nextbin
print('-' * 80)
print(','.join(sorted(currbinmembers)))
print('')
print('')
currbinmembers = []
for node in nodebydatum[datum]:
currbinmembers.append(node)
if currbinmembers:
print('Entries between {0} and {1}'.format(currbin, np.max(plotdata)))
print('-' * 80)
print(','.join(sorted(currbinmembers)))
print('')
print('')
-860
View File
@@ -1,860 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import ctypes
import ctypes.util
import dbm
import csv
import errno
import fnmatch
import hashlib
import os
import shlex
import socket
import ssl
import sys
import confluent.asynctlvdata as tlvdata
import confluent.sortutil as sortutil
libssl = ctypes.CDLL(ctypes.util.find_library('ssl'))
libssl.SSL_CTX_set_cert_verify_callback.argtypes = [
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
SO_PASSCRED = 16
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'switchuser': 'secret.hardwaremanagementuser',
'bmcpass': 'secret.hardwaremanagementpassword',
'switchpass': 'secret.hardwaremanagementpassword',
}
try:
getinput = raw_input
except NameError:
getinput = input
class PyObject_HEAD(ctypes.Structure):
_fields_ = [
("ob_refcnt", ctypes.c_ssize_t),
("ob_type", ctypes.c_void_p),
]
# see main/Modules/_ssl.c, only caring about the SSL_CTX pointer
class PySSLContext(ctypes.Structure):
_fields_ = [
("ob_base", PyObject_HEAD),
("ctx", ctypes.c_void_p),
]
@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def verify_stub(store, misc):
return 1
class NestedDict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
def stringify(instr):
# Normalize unicode and bytes to 'str', correcting for
# current python version
if isinstance(instr, bytes) and not isinstance(instr, str):
return instr.decode('utf-8', errors='replace')
elif not isinstance(instr, bytes) and not isinstance(instr, str):
return instr.encode('utf-8')
return instr
class Tabulator(object):
def __init__(self, headers):
self.headers = headers
self.rows = []
def add_row(self, row):
self.rows.append(row)
def get_table(self, order=None):
i = 0
fmtstr = ''
separator = []
for head in self.headers:
if order and order == head:
order = i
neededlen = len(head)
for row in self.rows:
if len(row[i]) > neededlen:
neededlen = len(row[i])
separator.append('-' * (neededlen + 1))
fmtstr += '{{{0}:>{1}}}|'.format(i, neededlen + 1)
i = i + 1
fmtstr = fmtstr[:-1]
yield fmtstr.format(*self.headers)
yield fmtstr.format(*separator)
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
yield fmtstr.format(*row)
else:
for row in self.rows:
yield fmtstr.format(*row)
def write_csv(self, output, order=None):
output = csv.writer(output)
output.writerow(self.headers)
i = 0
for head in self.headers:
if order and order == head:
order = i
i = i + 1
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
output.writerow(row)
else:
for row in self.rows:
output.writerow(row)
def printerror(res, node=None):
exitcode = 0
if 'errorcode' in res:
exitcode = res['errorcode']
for node in res.get('databynode', {}):
exitcode = res['databynode'][node].get('errorcode', exitcode)
if 'error' in res['databynode'][node]:
sys.stderr.write(
'{0}: {1}\n'.format(node, res['databynode'][node]['error']))
if exitcode == 0:
exitcode = 1
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
return exitcode
def cprint(txt):
try:
print(txt)
except UnicodeEncodeError:
print(txt.encode('utf8'))
sys.stdout.flush()
def _parseserver(string):
if ']:' in string:
server, port = string[1:].split(']:')
elif string[0] == '[':
server = string[1:-1]
port = '13001'
elif ':' in string:
server, port = string.split(':')
else:
server = string
port = '13001'
return server, port
class Command(object):
def __init__(self, server=None):
self._prevdict = None
self._prevkeyname = None
self.connection = None
self._currnoderange = None
self.unixdomain = False
if server is None:
if 'CONFLUENT_HOST' in os.environ:
self.serverloc = os.environ['CONFLUENT_HOST']
else:
self.serverloc = '/var/run/confluent/api.sock'
else:
self.serverloc = server
self.connected = False
async def ensure_connected(self):
if self.connected:
return True
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
self._connect_unix()
self.unixdomain = True
elif self.serverloc == '/var/run/confluent/api.sock':
raise Exception('Confluent service is not available')
else:
await self._connect_tls()
self.protversion = int((await tlvdata.recv(self.connection)).split(
b'--')[1].strip()[1:])
authdata = await tlvdata.recv(self.connection)
if authdata['authpassed'] == 1:
self.authenticated = True
else:
self.authenticated = False
if not self.authenticated and 'CONFLUENT_USER' in os.environ:
username = os.environ['CONFLUENT_USER']
passphrase = os.environ['CONFLUENT_PASSPHRASE']
await self.authenticate(username, passphrase)
self.connected = True
async def add_file(self, name, handle, mode):
await self.ensure_connected()
if self.protversion < 3:
raise Exception('Not supported with connected confluent server')
if not self.unixdomain:
raise Exception('Can only add a file to a unix domain connection')
await tlvdata.send(self.connection, {'filename': name, 'mode': mode}, handle)
async def authenticate(self, username, password):
await tlvdata.send(self.connection,
{'username': username, 'password': password})
authdata = await tlvdata.recv(self.connection)
if authdata['authpassed'] == 1:
self.authenticated = True
def add_precede_key(self, keyname):
self._prevkeyname = keyname
def add_precede_dict(self, dict):
self._prevdict = dict
def handle_results(self, ikey, rc, res, errnodes=None, outhandler=None):
if 'error' in res:
if errnodes is not None:
errnodes.add(self._currnoderange)
sys.stderr.write('Error: {0}\n'.format(res['error']))
if 'errorcode' in res:
return res['errorcode']
else:
return 1
if 'databynode' not in res:
return 0
res = res['databynode']
for node in res:
if 'error' in res[node]:
if errnodes is not None:
errnodes.add(node)
sys.stderr.write('{0}: Error: {1}\n'.format(
node, res[node]['error']))
if 'errorcode' in res[node]:
rc |= res[node]['errorcode']
else:
rc |= 1
elif ikey in res[node]:
if 'value' in res[node][ikey]:
val = res[node][ikey]['value']
elif 'isset' in res[node][ikey]:
val = '********' if res[node][ikey] else ''
else:
val = repr(res[node][ikey])
if self._prevkeyname and self._prevkeyname in res[node]:
cprint('{0}: {2}->{1}'.format(
node, val, res[node][self._prevkeyname]['value']))
elif self._prevdict and node in self._prevdict:
cprint('{0}: {2}->{1}'.format(
node, val, self._prevdict[node]))
else:
cprint('{0}: {1}'.format(node, val))
elif outhandler:
outhandler(node, res)
return rc
async def simple_noderange_command(self, noderange, resource, input=None,
key=None, errnodes=None, promptover=None, outhandler=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
async for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
else:
await self.stop_if_noderange_over(noderange, promptover)
kwargs[ikey] = input
async for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
self._currnoderange = None
return rc
except KeyboardInterrupt:
cprint('')
return 0
async def stop_if_noderange_over(self, noderange, maxnodes):
if maxnodes is None:
return
nsize = await self.get_noderange_size(noderange)
if nsize > maxnodes:
if nsize == 1:
nodename = [x async for x in self.read(
'/noderange/{0}/nodes/'.format(noderange))][0].get('item', {}).get('href', None)
nodename = nodename[:-1]
p = getinput('Command is about to affect node {0}, continue (y/n)? '.format(nodename))
else:
p = getinput('Command is about to affect {0} nodes, continue (y/n)? '.format(nsize))
if p.lower() != 'y':
sys.stderr.write('Aborting at user request\n')
sys.exit(1)
raise Exception("Aborting at user request")
async def get_noderange_size(self, noderange):
numnodes = 0
async for node in self.read('/noderange/{0}/nodes/'.format(noderange)):
if node.get('item', {}).get('href', None):
numnodes += 1
else:
raise Exception("Error trying to size noderange {0}".format(noderange))
return numnodes
async def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
try:
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
async for res in self.read('/nodegroups/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
else:
kwargs[ikey] = input
async for res in self.update('/nodegroups/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
cprint('')
return 0
async def read(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'retrieve', path, self.connection, parameters):
yield rsp
async def update(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'update', path, self.connection, parameters):
yield rsp
async def create(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'create', path, self.connection, parameters):
yield rsp
async def delete(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'delete', path, self.connection, parameters):
yield rsp
def _connect_unix(self):
self.connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.connection.setsockopt(socket.SOL_SOCKET, SO_PASSCRED, 1)
self.connection.connect(self.serverloc)
self.connection.setblocking(False)
async def _connect_tls(self):
server, port = _parseserver(self.serverloc)
for res in await asyncio.get_running_loop().getaddrinfo(
server, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
try:
self.connection = socket.socket(af, socktype, proto)
self.connection.setsockopt(
socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except:
self.connection = None
continue
try:
self.connection.settimeout(5)
self.connection.connect(sa)
self.connection.settimeout(0)
except:
raise
self.connection.close()
self.connection = None
continue
break
if self.connection is None:
raise Exception("Failed to connect to %s" % self.serverloc)
#TODO(jbjohnso): server certificate validation
clientcfgdir = os.path.join(os.path.expanduser("~"), ".confluent")
try:
os.makedirs(clientcfgdir)
except OSError as exc:
if not (exc.errno == errno.EEXIST and os.path.isdir(clientcfgdir)):
raise
cacert = os.path.join(clientcfgdir, "ca.pem")
certreqs = ssl.CERT_REQUIRED
knownhosts = False
if not os.path.exists(cacert):
cacert = None
certreqs = ssl.CERT_NONE
knownhosts = True
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ssl_ctx = PySSLContext.from_address(id(ctx)).ctx
libssl.SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_stub, 0)
sreader = asyncio.StreamReader()
sreaderprot = asyncio.StreamReaderProtocol(sreader)
cloop = asyncio.get_running_loop()
tport, _ = await cloop.create_connection(
lambda: sreaderprot, sock=self.connection, ssl=ctx, server_hostname='x')
swriter = asyncio.StreamWriter(tport, sreaderprot, sreader, cloop)
self.connection = (sreader, swriter)
#self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
# cert_reqs=certreqs)
if knownhosts:
certdata = tport.get_extra_info('ssl_object').getpeercert(binary_form=True)
# certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
fingerprint = fingerprint.encode('utf-8')
hostid = '@'.join((port, server))
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
if hostid in khf:
if fingerprint == khf[hostid]:
return
else:
replace = getinput(
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
cprint('Adding new key for %s:%s' % (server, port))
khf[hostid] = fingerprint
async def send_request(operation, path, server, parameters=None):
"""This function iterates over all the responses
received from the server.
:param operation: The operation to request, retrieve, update, delete,
create, start, stop
:param path: The URI path to the resource to operate on
:param server: The socket to send data over
:param parameters: Parameters if any to send along with the request
"""
payload = {'operation': operation, 'path': path}
if parameters is not None:
payload['parameters'] = parameters
await tlvdata.send(server, payload)
result = await tlvdata.recv(server)
while '_requestdone' not in result:
try:
yield result
except GeneratorExit:
while '_requestdone' not in result:
result = await tlvdata.recv(server)
raise
result = await tlvdata.recv(server)
def attrrequested(attr, attrlist, seenattributes, node=None):
for candidate in attrlist:
truename = candidate
if candidate.startswith('hm'):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate in _attraliases:
candidate = _attraliases[candidate]
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = True
return True
elif attr.lower().startswith(candidate.lower() + '.'):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = 1
return True
return False
async def printattributes(session, requestargs, showtype, nodetype, noderange, options):
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
return await print_attrib_path(path, session, requestargs, options)
def _sort_attrib(k):
if isinstance(k[1], dict) and k[1].get('sortid', None) is not None:
return sortutil.naturalize_string('{}'.format(k[1]['sortid']))
return sortutil.naturalize_string(k[0])
def _is_nondefault(currattr):
if currattr.get('default', None) is None:
return False # no default specified cannot tell
if 'value' not in currattr: # can't compare if no value specified
return False
if currattr['value'] != currattr['default']:
return True
# ok, the potentially pending value is the same, but check active
if 'active' not in currattr:
return False
if currattr['active'] != currattr['default']:
return True
return False
async def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None, showpending=False):
exitcode = 0
seenattributes = NestedDict()
allnodes = set([])
async for res in session.read(path):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in sorted(res['databynode']):
allnodes.add(node)
for attr, val in sorted(res['databynode'][node].items(), key=_sort_attrib):
if attr == 'error':
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
continue
if attr == 'errorcode':
exitcode |= val
continue
seenattributes[node][attr] = True
if rename:
printattr = rename.get(attr, attr)
else:
printattr = attr
if attrprefix:
printattr = attrprefix + printattr
currattr = res['databynode'][node][attr]
if show_attr(attr, requestargs, seenattributes, options, node):
if 'value' in currattr:
currval = currattr.get('value', None)
if isinstance(currval, list):
currval = ','.join(currval)
activeval = currattr.get('active', currval)
if isinstance(activeval, list):
activeval = ','.join(activeval)
if activeval != currval:
if currval is None:
currval = ''
if activeval is None:
activeval = ''
if showpending:
attrout = f'{node}: {printattr}: {activeval} -> {currval}'
else:
attrout = f'{node}: {printattr}: {currval} (Pending)'
elif showpending:
continue
elif currval is not None:
attrout = '{0}: {1}: {2}'.format(
node, printattr, currval).strip()
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node,
printattr)
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif isinstance(currattr, dict) and 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(node, printattr,
currattr['broken'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
else:
cprint("CODE ERROR" + repr(attr))
try:
blame = options.blame
except AttributeError:
blame = False
if blame or (isinstance(currattr, dict) and 'broken' in currattr):
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
currattr['inheritedfrom']
))
if 'expression' in currattr:
blamedata.append(
'derived from expression "{0}"'.format(
currattr['expression']))
if blamedata:
attrout += ' (' + ', '.join(blamedata) + ')'
try:
comparedefault = options.comparedefault
except AttributeError:
comparedefault = False
if comparedefault:
try:
exclude = options.exclude
except AttributeError:
exclude = False
if ((requestargs and not exclude) or
_is_nondefault(currattr)):
currval = ','.join(currattr['value']) if isinstance(
currattr['value'], list) else currattr['value']
activeval = ','.join(currattr['active']) if isinstance(
currattr.get('active'), list) else currattr.get('active')
dval = ','.join(currattr['default']) if isinstance(
currattr['default'], list) else currattr['default']
outmsg = '{0}: {1}: {2} (Default: {3}'.format(
node, printattr, currval, dval)
if 'active' in currattr:
outmsg += ', Pending change from: {0}'.format(activeval)
outmsg += ')'
cprint(outmsg)
else:
try:
details = options.detail
except AttributeError:
details = False
if details:
if currattr.get('help', None):
attrout += u' (Help: {0})'.format(
currattr['help'])
if currattr.get('possible', None):
try:
attrout += u' (Choices: {0})'.format(
','.join(currattr['possible']))
except TypeError:
pass
cprint(attrout)
somematched = set([])
printmissing = set([])
badnodes = NestedDict()
if not exitcode:
if requestargs:
for attr in requestargs:
for node in allnodes:
if attr in seenattributes[node]:
somematched.add(attr)
else:
badnodes[node][attr] = True
exitcode = 1
for node in sortutil.natural_sort(badnodes):
for attr in badnodes[node]:
if attr in somematched:
sys.stderr.write(
'Error: {0} matches no valid value for {1}\n'.format(
attr, node))
else:
printmissing.add(attr)
for missing in printmissing:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(missing))
return exitcode
def show_attr(attr, requestargs, seenattributes, options, node):
try:
reverse = options.exclude
except AttributeError:
reverse = False
if requestargs is None or requestargs == []:
return True
processattr = attrrequested(attr, requestargs, seenattributes, node)
if reverse:
processattr = not processattr
return processattr
async def printgroupattributes(session, requestargs, showtype, nodetype, noderange, options):
exitcode = 0
seenattributes = set([])
async for res in session.read('/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for attr in res:
seenattributes.add(attr)
currattr = res[attr]
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
if 'value' in currattr:
if currattr['value'] is not None:
attrout = '{0}: {1}: {2}'.format(
noderange, attr, currattr['value'])
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(noderange, attr)
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif isinstance(currattr, dict) and 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(noderange, attr,
currattr['broken'])
elif 'expression' in currattr:
attrout = '{0}: {1}: (will derive from expression {2})'.format(noderange, attr, currattr['expression'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, dictout)))
else:
cprint("CODE ERROR" + repr(attr))
cprint(attrout)
if not exitcode:
if requestargs:
for attr in requestargs:
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
exitcode = 1
return exitcode
async def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
# update attribute
exitcode = 0
if options.clear:
targpath = '/{0}/{1}/attributes/all'.format(nodetype, noderange)
keydata = {}
for attrib in updateargs[1:]:
keydata[attrib] = None
async for res in session.update(targpath, keydata):
for node in res.get('databynode', {}):
for warnmsg in res['databynode'][node].get('_warnings', []):
sys.stderr.write('Warning: ' + warnmsg + '\n')
if 'error' in res:
if 'errorcode' in res:
exitcode = res['errorcode']
sys.stderr.write('Error: ' + res['error'] + '\n')
sys.exit(exitcode)
elif hasattr(options, 'environment') and options.environment:
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
# Let's do one pass to make sure that there's not a usage problem
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
if (nodetype == "nodegroups"):
exitcode = await session.simple_nodegroups_command(noderange,
'attributes/all',
value, key)
else:
exitcode = await session.simple_noderange_command(noderange,
'attributes/all',
value, key)
sys.exit(exitcode)
elif dictassign:
for key in dictassign:
if nodetype == 'nodegroups':
exitcode = await session.simple_nodegroups_command(
noderange, 'attributes/all', dictassign[key], key)
else:
exitcode = await session.simple_noderange_command(
noderange, 'attributes/all', dictassign[key], key)
else:
if "=" in updateargs[1]:
update_ready = True
for arg in updateargs[1:]:
if '=' not in arg:
update_ready = False
exitcode = 1
if not update_ready:
sys.stderr.write('Error: {0} Can not set and read at the same time!\n'.format(str(updateargs[1:])))
sys.exit(exitcode)
try:
for val in updateargs[1:]:
val = val.split('=', 1)
if val[0][-1] in (',', '-', '^'):
key = val[0][:-1]
if val[0][-1] == ',':
value = {'prepend': val[1]}
elif val[0][-1] in ('-', '^'):
value = {'remove': val[1]}
else:
key = val[0]
value = val[1]
if (nodetype == "nodegroups"):
exitcode = await session.simple_nodegroups_command(noderange, 'attributes/all',
value, key)
else:
exitcode = await session.simple_noderange_command(noderange, 'attributes/all',
value, key)
except Exception:
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
exitcode = 1
sys.exit(exitcode)
return exitcode
# So we try to prevent bad things from happening when globbing
# We tried to head this off at the shell, but the various solutions would end
# up breaking the shell in various ways (breaking pipe capability if using
# DEBUG, breaking globbing if in pipe, etc)
# Then we tried to parse the original commandline instead, however shlex isn't
# going to parse full bourne language (e.g. knowing that '|' and '>' and
# a world of other things would not be in our command line
# so finally, just make sure the noderange appears verbatim in the command line
# if we glob to something, then bash will change noderange and this should
# detect it and save the user from tragedy
def check_globbing(noderange):
if not os.path.exists(noderange):
return True
rawargs = os.environ.get('CURRENT_CMDLINE', None)
if rawargs:
rawargs = shlex.split(rawargs)
for arg in rawargs:
if arg.startswith('$'):
arg = arg[1:]
if arg.endswith(';'):
arg = arg[:-1]
arg = os.environ.get(arg, '$' + arg)
if arg.startswith(noderange):
break
else:
sys.stderr.write(
'Shell glob conflict detected, specified target "{0}" '
'not in command line, but is a file. You can use "set -f" in '
'bash or change directories such that there is no filename '
'that would conflict.'
'\n'.format(noderange))
sys.exit(1)
-313
View File
@@ -1,313 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import array
import asyncio
import ctypes
import ctypes.util
import confluent.tlv as tlv
import socket
from datetime import datetime
import json
import os
import struct
try:
unicode
except NameError:
unicode = str
try:
range = xrange
except NameError:
pass
class iovec(ctypes.Structure): # from uio.h
_fields_ = [('iov_base', ctypes.c_void_p),
('iov_len', ctypes.c_size_t)]
iovec_ptr = ctypes.POINTER(iovec)
class cmsghdr(ctypes.Structure): # also from bits/socket.h
_fields_ = [('cmsg_len', ctypes.c_size_t),
('cmsg_level', ctypes.c_int),
('cmsg_type', ctypes.c_int)]
@classmethod
def init_data(cls, cmsg_len, cmsg_level, cmsg_type, cmsg_data):
Data = ctypes.c_ubyte * ctypes.sizeof(cmsg_data)
class _flexhdr(ctypes.Structure):
_fields_ = cls._fields_ + [('cmsg_data', Data)]
datab = Data(*bytearray(cmsg_data))
return _flexhdr(cmsg_len=cmsg_len, cmsg_level=cmsg_level,
cmsg_type=cmsg_type, cmsg_data=datab)
def CMSG_LEN(length):
sizeof_cmshdr = ctypes.sizeof(cmsghdr)
return ctypes.c_size_t(CMSG_ALIGN(sizeof_cmshdr).value + length)
SCM_RIGHTS = 1
class msghdr(ctypes.Structure): # from bits/socket.h
_fields_ = [('msg_name', ctypes.c_void_p),
('msg_namelen', ctypes.c_uint),
('msg_iov', ctypes.POINTER(iovec)),
('msg_iovlen', ctypes.c_size_t),
('msg_control', ctypes.c_void_p),
('msg_controllen', ctypes.c_size_t),
('msg_flags', ctypes.c_int)]
def CMSG_ALIGN(length): # bits/socket.h
ret = (length + ctypes.sizeof(ctypes.c_size_t) - 1
& ~(ctypes.sizeof(ctypes.c_size_t) - 1))
return ctypes.c_size_t(ret)
def CMSG_SPACE(length): # bits/socket.h
ret = CMSG_ALIGN(length).value + CMSG_ALIGN(ctypes.sizeof(cmsghdr)).value
return ctypes.c_size_t(ret)
class ClientFile(object):
def __init__(self, name, mode, fd):
self.fileobject = os.fdopen(fd, mode)
self.filename = name
def _sendmsg(loop, fut, sock, msg, fds, wfd):
if wfd is not None:
loop.remove_writer(wfd)
if fut.cancelled():
return
try:
retdata = sock.sendmsg(
[msg],
[(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds))])
except (BlockingIOError, InterruptedError):
fd = sock.fileno()
loop.add_writer(fd, _sendmsg, loop, fut, sock, msg, fds, fd)
except Exception as exc:
fut.set_exception(exc)
else:
fut.set_result(retdata)
def send_fds(sock, msg, fds):
cloop = asyncio.get_running_loop()
fut = cloop.create_future()
_sendmsg(cloop, fut, sock, msg, fds, None)
return fut
def _recvmsg(loop, fut, sock, msglen, maxfds, rfd):
if rfd is not None:
loop.remove_reader(rfd)
if fut.cancelled():
return
fds = array.array("i") # Array of ints
try:
msg, ancdata, flags, addr = sock.recvmsg(
msglen, socket.CMSG_LEN(maxfds * fds.itemsize))
except (BlockingIOError, InterruptedError):
fd = sock.fileno()
loop.add_reader(fd, _recvmsg, loop, fut, sock, msglen, maxfds, fd)
except Exception as exc:
fut.set_exception(exc)
else:
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if (cmsg_level == socket.SOL_SOCKET
and cmsg_type == socket.SCM_RIGHTS):
# Append data, ignoring any truncated integers at the end.
fds.frombytes(
cmsg_data[
:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
fut.set_result((msg, list(fds)))
def recv_fds(sock, msglen, maxfds):
cloop = asyncio.get_running_loop()
fut = cloop.create_future()
_recvmsg(cloop, fut, sock, msglen, maxfds, None)
return fut
def decodestr(value):
ret = None
try:
ret = value.decode('utf-8')
except UnicodeDecodeError:
try:
ret = value.decode('cp437')
except UnicodeDecodeError:
ret = value
except AttributeError:
return value
return ret
def unicode_dictvalues(dictdata):
for key in dictdata:
if isinstance(dictdata[key], bytes):
dictdata[key] = decodestr(dictdata[key])
elif isinstance(dictdata[key], datetime):
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(dictdata[key], list):
_unicode_list(dictdata[key])
elif isinstance(dictdata[key], dict):
unicode_dictvalues(dictdata[key])
def _unicode_list(currlist):
for i in range(len(currlist)):
if isinstance(currlist[i], str):
currlist[i] = decodestr(currlist[i])
elif isinstance(currlist[i], dict):
unicode_dictvalues(currlist[i])
elif isinstance(currlist[i], list):
_unicode_list(currlist[i])
async def sendall(handle, data):
if isinstance(handle, tuple):
handle[1].write(data)
return await handle[1].drain()
else:
cloop = asyncio.get_running_loop()
return await cloop.sock_sendall(handle, data)
def get_socket(handle):
if isinstance(handle, tuple):
return handle[1].transport.get_extra_info('socket')
else:
return handle
async def close(handle):
if isinstance(handle, tuple):
handle[1].close()
try:
await handle[1].wait_closed()
except Exception:
pass
else:
handle.close()
async def send(handle, data, filehandle=None):
cloop = asyncio.get_running_loop()
if isinstance(data, unicode):
try:
data = data.encode('utf-8')
except AttributeError:
pass
if isinstance(data, bytes) or isinstance(data, unicode):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
# if you don't have anything to say, don't say anything at all
return
if tl < 16777216:
# type for string is '0', so we don't need
# to xor anything in
await sendall(handle, struct.pack("!I", tl))
else:
raise Exception("String data length exceeds protocol")
await sendall(handle, data)
elif isinstance(data, dict): # JSON currently only goes to 4 bytes
# Some structured message, like what would be seen in http responses
unicode_dictvalues(data) # make everything unicode, assuming UTF-8
sdata = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
sdata = sdata.encode('utf-8')
tl = len(sdata)
if tl > 16777215:
raise Exception("JSON data exceeds protocol limits")
# xor in the type (0b1 << 24)
if filehandle is None:
tl |= 16777216
await sendall(handle, struct.pack("!I", tl))
await sendall(handle, sdata)
elif isinstance(handle, tuple):
raise Exception("Cannot send filehandle over network socket")
else:
tl |= (2 << 24)
await cloop.sock_sendall(handle, struct.pack("!I", tl))
await send_fds(handle, sdata, [filehandle])
async def _grabhdl(handle, size):
if isinstance(handle, tuple):
return await handle[0].read(size)
else:
cloop = asyncio.get_running_loop()
return await cloop.sock_recv(handle, size)
async def recvall(handle, size):
rd = await _grabhdl(handle, size)
while len(rd) < size:
nd = await _grabhdl(handle, size - len(rd))
if not nd:
raise Exception("Error reading data")
rd += nd
return rd
async def recv(handle):
tl = await _grabhdl(handle, 4)
if not tl:
return None
while len(tl) < 4:
ndata = await _grabhdl(handle, 4 - len(tl))
if not ndata:
raise Exception("Error reading data")
tl += ndata
if len(tl) == 0:
return None
tl = struct.unpack("!I", tl)[0]
if tl & 0b10000000000000000000000000000000:
raise Exception("Protocol Violation, reserved bit set")
# 4 byte tlv
dlen = tl & 16777215 # grab lower 24 bits
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
if dlen == 0:
return None
if datatype == tlv.Types.filehandle:
if isinstance(handle, tuple):
raise Exception('Filehandle not supported over TLS socket')
msg, filehandles = await recv_fds(handle, dlen, 4)
data = json.loads(bytes(msg))
return ClientFile(data['filename'], data['mode'], filehandles[0])
else:
data = await _grabhdl(handle, dlen)
while len(data) < dlen:
ndata = await _grabhdl(handle, dlen - len(data))
if not ndata:
raise Exception("Error reading data")
data += ndata
if datatype == tlv.Types.text:
return data
elif datatype == tlv.Types.json:
return json.loads(data)
+55 -218
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2019 Lenovo
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,10 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
try:
import anydbm as dbm
except ImportError:
import dbm
import anydbm as dbm
import csv
import errno
import fnmatch
@@ -36,31 +33,9 @@ SO_PASSCRED = 16
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'switchuser': 'secret.hardwaremanagementuser',
'bmcpass': 'secret.hardwaremanagementpassword',
'switchpass': 'secret.hardwaremanagementpassword',
}
try:
getinput = raw_input
except NameError:
getinput = input
class NestedDict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
def stringify(instr):
# Normalize unicode and bytes to 'str', correcting for
# current python version
if isinstance(instr, bytes) and not isinstance(instr, str):
return instr.decode('utf-8', errors='replace')
elif not isinstance(instr, bytes) and not isinstance(instr, str):
return instr.encode('utf-8')
return instr
class Tabulator(object):
def __init__(self, headers):
@@ -118,12 +93,6 @@ def printerror(res, node=None):
exitcode = 0
if 'errorcode' in res:
exitcode = res['errorcode']
for node in res.get('databynode', {}):
exitcode = res['databynode'][node].get('errorcode', exitcode)
if 'error' in res['databynode'][node]:
sys.stderr.write('{0}: {1}\n'.format(node, res['databynode'][node]['error']))
if exitcode == 0:
exitcode = 1
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
@@ -135,10 +104,7 @@ def printerror(res, node=None):
def cprint(txt):
try:
print(txt)
except UnicodeEncodeError:
print(txt.encode('utf8'))
print(txt)
sys.stdout.flush()
def _parseserver(string):
@@ -161,7 +127,6 @@ class Command(object):
self._prevkeyname = None
self.connection = None
self._currnoderange = None
self.unixdomain = False
if server is None:
if 'CONFLUENT_HOST' in os.environ:
self.serverloc = os.environ['CONFLUENT_HOST']
@@ -171,13 +136,11 @@ class Command(object):
self.serverloc = server
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
self._connect_unix()
self.unixdomain = True
elif self.serverloc == '/var/run/confluent/api.sock':
raise Exception('Confluent service is not available')
else:
self._connect_tls()
self.protversion = int(tlvdata.recv(self.connection).split(
b'--')[1].strip()[1:])
tlvdata.recv(self.connection)
authdata = tlvdata.recv(self.connection)
if authdata['authpassed'] == 1:
self.authenticated = True
@@ -188,13 +151,6 @@ class Command(object):
passphrase = os.environ['CONFLUENT_PASSPHRASE']
self.authenticate(username, passphrase)
def add_file(self, name, handle, mode):
if self.protversion < 3:
raise Exception('Not supported with connected confluent server')
if not self.unixdomain:
raise Exception('Can only add a file to a unix domain connection')
tlvdata.send(self.connection, {'filename': name, 'mode': mode}, handle)
def authenticate(self, username, password):
tlvdata.send(self.connection,
{'username': username, 'password': password})
@@ -208,7 +164,7 @@ class Command(object):
def add_precede_dict(self, dict):
self._prevdict = dict
def handle_results(self, ikey, rc, res, errnodes=None, outhandler=None):
def handle_results(self, ikey, rc, res, errnodes=None):
if 'error' in res:
if errnodes is not None:
errnodes.add(self._currnoderange)
@@ -245,12 +201,10 @@ class Command(object):
node, val, self._prevdict[node]))
else:
cprint('{0}: {1}'.format(node, val))
elif outhandler:
outhandler(node, res)
return rc
def simple_noderange_command(self, noderange, resource, input=None,
key=None, errnodes=None, promptover=None, outhandler=None, **kwargs):
key=None, errnodes=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
@@ -264,46 +218,17 @@ class Command(object):
if input is None:
for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
rc = self.handle_results(ikey, rc, res, errnodes)
else:
self.stop_if_noderange_over(noderange, promptover)
kwargs[ikey] = input
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
rc = self.handle_results(ikey, rc, res, errnodes)
self._currnoderange = None
return rc
except KeyboardInterrupt:
cprint('')
return 0
def stop_if_noderange_over(self, noderange, maxnodes):
if maxnodes is None:
return
maxnodes = int(maxnodes)
nsize = self.get_noderange_size(noderange)
if nsize > maxnodes:
if nsize == 1:
nodename = list(self.read(
'/noderange/{0}/nodes/'.format(noderange)))[0].get('item', {}).get('href', None)
nodename = nodename[:-1]
p = getinput('Command is about to affect node {0}, continue (y/n)? '.format(nodename))
else:
p = getinput('Command is about to affect {0} nodes, continue (y/n)? '.format(nsize))
if p.lower() != 'y':
sys.stderr.write('Aborting at user request\n')
sys.exit(1)
raise Exception("Aborting at user request")
def get_noderange_size(self, noderange):
numnodes = 0
for node in self.read('/noderange/{0}/nodes/'.format(noderange)):
if node.get('item', {}).get('href', None):
numnodes += 1
else:
raise Exception("Error trying to size noderange {0}".format(noderange))
return numnodes
def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
try:
@@ -392,24 +317,18 @@ class Command(object):
cacert = None
certreqs = ssl.CERT_NONE
knownhosts = True
tlsctx = ssl.create_default_context()
if certreqs == ssl.CERT_NONE:
tlsctx.check_hostname = False
tlsctx.verify_mode = certreqs
if cacert:
tlsctx.load_verify_locations(cacert)
self.connection = tlsctx.wrap_socket(self.connection, server_hostname=server)
self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
cert_reqs=certreqs)
if knownhosts:
certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
fingerprint = fingerprint.encode('utf-8')
hostid = '@'.join((port, server))
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
if hostid in khf:
if fingerprint == khf[hostid]:
return
else:
replace = getinput(
replace = raw_input(
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
@@ -442,7 +361,7 @@ def send_request(operation, path, server, parameters=None):
result = tlvdata.recv(server)
def attrrequested(attr, attrlist, seenattributes, node=None):
def attrrequested(attr, attrlist, seenattributes):
for candidate in attrlist:
truename = candidate
if candidate.startswith('hm'):
@@ -450,16 +369,10 @@ def attrrequested(attr, attrlist, seenattributes, node=None):
if candidate in _attraliases:
candidate = _attraliases[candidate]
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = True
seenattributes.add(truename)
return True
elif attr.lower().startswith(candidate.lower() + '.'):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = 1
seenattributes.add(truename)
return True
return False
@@ -468,73 +381,39 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, options
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
return print_attrib_path(path, session, requestargs, options)
def _sort_attrib(k):
if isinstance(k[1], dict) and k[1].get('sortid', None) is not None:
return sortutil.naturalize_string('{}'.format(k[1]['sortid']))
return sortutil.naturalize_string(k[0])
def _is_nondefault(currattr):
if currattr.get('default', None) is None:
return False # no default specified cannot tell
if 'value' not in currattr: # can't compare if no value specified
return False
if currattr['value'] != currattr['default']:
return True
# ok, the potentially pending value is the same, but check active
if 'active' not in currattr:
return False
if currattr['active'] != currattr['default']:
return True
return False
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None, showpending=False):
def print_attrib_path(path, session, requestargs, options, rename=None):
exitcode = 0
seenattributes = NestedDict()
allnodes = set([])
seenattributes = set([])
for res in session.read(path):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in sorted(res['databynode']):
allnodes.add(node)
for attr, val in sorted(res['databynode'][node].items(), key=_sort_attrib):
for attr, val in sorted(
res['databynode'][node].items(),
key=lambda (k, v): v.get('sortid', k) if isinstance(v, dict) else k):
if attr == 'error':
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
continue
if attr == 'errorcode':
exitcode |= val
continue
seenattributes[node][attr] = True
seenattributes.add(attr)
if rename:
printattr = rename.get(attr, attr)
else:
printattr = attr
if attrprefix:
printattr = attrprefix + printattr
currattr = res['databynode'][node][attr]
if show_attr(attr, requestargs, seenattributes, options, node):
if show_attr(attr, requestargs, seenattributes, options):
if 'value' in currattr:
currval = currattr.get('value', None)
if isinstance(currval, list):
currval = ','.join(currval)
activeval = currattr.get('active', currval)
if isinstance(activeval, list):
activeval = ','.join(activeval)
if activeval != currval:
if currval is None:
currval = ''
if activeval is None:
activeval = ''
if showpending:
attrout = f'{node}: {printattr}: {activeval} -> {currval}'
else:
attrout = f'{node}: {printattr}: {currval} (Pending)'
elif showpending:
continue
elif currval is not None:
if currattr['value'] is not None:
val = currattr['value']
if isinstance(val, list):
val = ','.join(val)
attrout = '{0}: {1}: {2}'.format(
node, printattr, currval).strip()
node, printattr, val)
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif 'isset' in currattr:
@@ -543,7 +422,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
printattr)
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif isinstance(currattr, dict) and 'broken' in currattr:
elif 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(node, printattr,
currattr['broken'])
@@ -560,7 +439,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
blame = options.blame
except AttributeError:
blame = False
if blame or (isinstance(currattr, dict) and 'broken' in currattr):
if blame or 'broken' in currattr:
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
@@ -582,19 +461,15 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
except AttributeError:
exclude = False
if ((requestargs and not exclude) or
_is_nondefault(currattr)):
(currattr.get('default', None) is not None and
currattr.get('value', None) is not None and
currattr['value'] != currattr['default'])):
cval = ','.join(currattr['value']) if isinstance(
currattr['value'], list) else currattr['value']
activeval = ','.join(currattr['active']) if isinstance(
currattr.get('active'), list) else currattr.get('active')
dval = ','.join(currattr['default']) if isinstance(
currattr['default'], list) else currattr['default']
outmsg = '{0}: {1}: {2} (Default: {3}'.format(
node, printattr, cval, dval)
if 'active' in currattr:
outmsg += ', Pending change from: {0}'.format(activeval)
outmsg += ')'
cprint(outmsg)
cprint('{0}: {1}: {2} (Default: {3})'.format(
node, printattr, cval, dval))
else:
try:
@@ -603,48 +478,29 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
details = False
if details:
if currattr.get('help', None):
attrout += u' (Help: {0})'.format(
currattr['help'])
attrout += ' (Help: {0})'.format(
currattr['help'].encode('utf-8'))
if currattr.get('possible', None):
try:
attrout += u' (Choices: {0})'.format(
','.join(currattr['possible']))
except TypeError:
pass
attrout += ' (Choices: {0})'.format(
','.join(currattr['possible']))
cprint(attrout)
somematched = set([])
printmissing = set([])
badnodes = NestedDict()
if not exitcode:
if requestargs:
for attr in requestargs:
for node in allnodes:
if attr in seenattributes[node]:
somematched.add(attr)
else:
badnodes[node][attr] = True
exitcode = 1
for node in sortutil.natural_sort(badnodes):
for attr in badnodes[node]:
if attr in somematched:
sys.stderr.write(
'Error: {0} matches no valid value for {1}\n'.format(
attr, node))
else:
printmissing.add(attr)
for missing in printmissing:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(missing))
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
exitcode = 1
return exitcode
def show_attr(attr, requestargs, seenattributes, options, node):
def show_attr(attr, requestargs, seenattributes, options):
try:
reverse = options.exclude
except AttributeError:
reverse = False
if requestargs is None or requestargs == []:
return True
processattr = attrrequested(attr, requestargs, seenattributes, node)
processattr = attrrequested(attr, requestargs, seenattributes)
if reverse:
processattr = not processattr
return processattr
@@ -673,7 +529,7 @@ def printgroupattributes(session, requestargs, showtype, nodetype, noderange, op
attrout = '{0}: {1}: ********'.format(noderange, attr)
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif isinstance(currattr, dict) and 'broken' in currattr:
elif 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(noderange, attr,
currattr['broken'])
@@ -706,40 +562,29 @@ def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=N
for attrib in updateargs[1:]:
keydata[attrib] = None
for res in session.update(targpath, keydata):
for node in res.get('databynode', {}):
for warnmsg in res['databynode'][node].get('_warnings', []):
sys.stderr.write('Warning: ' + warnmsg + '\n')
if 'error' in res:
if 'errorcode' in res:
exitcode = res['errorcode']
sys.stderr.write('Error: ' + res['error'] + '\n')
sys.exit(exitcode)
elif hasattr(options, 'environment') and options.environment:
# The environment variable name substitutes '_' for '.'
attrvalues = {}
# Let's do one pass to make sure that there's not a usage problem
for attrib in updateargs[1:]:
envkey = attrib.replace('.', '_')
if envkey in os.environ:
attrvalues[attrib] = os.environ[envkey]
elif envkey.upper() in os.environ:
attrvalues[attrib] = os.environ[envkey.upper()]
else:
sys.stderr.write(
'Error: {0} requested, but neither {1} nor {2} is set in '
'the environment\n'.format(attrib, envkey, envkey.upper()))
sys.exit(1)
for attrib in updateargs[1:]:
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
# Let's do one pass to make sure that there's not a usage problem
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
if (nodetype == "nodegroups"):
exitcode = session.simple_nodegroups_command(noderange,
'attributes/all',
attrvalues[attrib],
attrib)
value, key)
else:
exitcode = session.simple_noderange_command(noderange,
'attributes/all',
attrvalues[attrib],
attrib)
value, key)
sys.exit(exitcode)
elif dictassign:
for key in dictassign:
@@ -751,14 +596,6 @@ def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=N
noderange, 'attributes/all', dictassign[key], key)
else:
if "=" in updateargs[1]:
update_ready = True
for arg in updateargs[1:]:
if '=' not in arg:
update_ready = False
exitcode = 1
if not update_ready:
sys.stderr.write('Error: {0} Can not set and read at the same time!\n'.format(str(updateargs[1:])))
sys.exit(exitcode)
try:
for val in updateargs[1:]:
val = val.split('=', 1)
@@ -815,4 +652,4 @@ def check_globbing(noderange):
'bash or change directories such that there is no filename '
'that would conflict.'
'\n'.format(noderange))
sys.exit(1)
sys.exit(1)
-346
View File
@@ -1,346 +0,0 @@
#!/usr/bin/python3
import collections
import os
import struct
import sys
import time
import fcntl
import select
import termios
import tty
def writeout(data):
if isinstance(data, str) and not isinstance(data, bytes):
data = data.encode('utf8')
try:
if hasattr(sys.stdout, 'buffer'):
sys.stdout.buffer.write(data)
else:
sys.stdout.write(data)
except IOError:
time.sleep(0.1)
pass
class LogReplay(object):
def __init__(self, logfile, cblfile):
self.bin = open(cblfile, 'rb')
self.txt = open(logfile, 'rb')
self.cleardata = []
self.clearidx = 0
self.pendingdata = collections.deque([])
self.priordata = collections.deque([])
self.laststamp = None
self.needclear = False
self.lasttxt = ''
def _rewind(self, datasize=None):
curroffset = self.bin.tell() - 16
if self.cleardata and self.clearidx > 1:
self.clearidx -= 1
priordata = self.cleardata[self.clearidx - 1]
return curroffset, priordata
self.cleardata = []
self.clearidx = 0
newoffset = curroffset - 32
if newoffset < 0: #TODO: Follow a log roll
newoffset = 0
if datasize:
while datasize > 0 and newoffset > 0:
self.bin.seek(newoffset)
tmprec = self.bin.read(16)
newoffset -= 32
tmprec = struct.unpack('!BBIHIBBH', tmprec)
if tmprec[1] == 2:
datasize -= tmprec[3]
if newoffset >= 0:
self.bin.seek(newoffset)
return curroffset, None
def debuginfo(self):
return '{0}, {1}'.format(self.bin.tell(), self.clearidx)
def get_output(self, reverse=False):
endoffset = None
output = b''
if reverse: # Forget the uncommited future, if present
output += b'\x1b[2J\x1b[H'
endoffset, priordata = self._rewind(4096)
if priordata is not None:
return priordata, 1
elif self.needclear:
output += b'\x1b[2J\x1b[H'
self.needclear = False
if self.cleardata and self.clearidx < len(self.cleardata):
datachunk = self.cleardata[self.clearidx]
self.clearidx += 1
return datachunk, 1
self.cleardata = []
self.clearidx = 0
while (not reverse) or (self.bin.tell() < endoffset):
record = self.bin.read(16)
if not record:
return b'', 0
record = struct.unpack('!BBIHIBBH', record)
if record[0] > 16:
# Unsupported record, skip
self.bin.seek(record[0] - 16, 1)
continue
type = record[1]
offset = record[2]
size = record[3]
evtdata = record[5]
auxdata = record[6]
if type == 3:
#TODO: provide data for status bar
continue
elif type == 2:
self.laststamp = record[4]
self.txt.seek(offset)
txtout = self.txt.read(size)
if reverse and self.bin.tell() < endoffset:
output += txtout
continue
self.paginate(txtout)
if self.cleardata:
self.clearidx = 0
if not self.cleardata[0]:
self.cleardata = self.cleardata[1:]
if self.cleardata:
if reverse:
output = self.cleardata[-1]
self.clearidx = len(self.cleardata)
else:
output += self.cleardata[0]
self.clearidx = 1
else:
output += txtout
break
if endoffset is not None and endoffset >= 0:
self.bin.seek(endoffset)
self.lasttxt = output
return output, 1
def paginate(self, txtout):
cleardata = [txtout]
nextcleardata = []
for sep in (b'\x1b[2J', b'\x1b[H\x1b[J'):
replacementcleardata = []
for txtout in cleardata:
nextcleardata = txtout.split(sep)
if len(nextcleardata) > 1:
for idx in range(1, len(nextcleardata)):
nextcleardata[idx] = sep + nextcleardata[idx]
replacementcleardata.extend(nextcleardata)
nextcleardata = []
cleardata = replacementcleardata
if len(cleardata) > 1:
self.cleardata = cleardata
return
def begin(self):
self.needclear = True
self.bin.seek(0)
def end(self):
self.bin.seek(0, 2)
def search(self, searchstr, skipfirst=False):
overlap = len(searchstr)
firstoffset = self.bin.tell()
lastoffset = firstoffset
output = self.lasttxt
while searchstr not in output:
output, delay = self.get_output()
if not self.cleardata and lastoffset == self.bin.tell():
self.bin.seek(firstoffset)
return b'', 0
lastoffset = self.bin.tell()
if skipfirst:
output, delay = self.get_output()
while searchstr not in output:
output, delay = self.get_output()
if not self.cleardata and lastoffset == self.bin.tell():
self.bin.seek(firstoffset)
return b'', 0
lastoffset = self.bin.tell()
clear = b'\x1b[2J\x1b[H'
txtchunks = self.lasttxt.split(searchstr)
output = clear + txtchunks[0]
txtchunks = txtchunks[1:]
while txtchunks:
output += b'\x1b[7m' + searchstr + b'\x1b[27m' + txtchunks[0]
txtchunks = txtchunks[1:]
return output, 0
def _replay_to_console(txtfile, binfile):
if not sys.stdin.isatty():
# Interactive replay needs a terminal to put in raw mode and to take
# navigation keys from, so without one just write the log out
return dump_to_console(txtfile)
replay = LogReplay(txtfile, binfile)
oldtcattr = termios.tcgetattr(sys.stdin.fileno())
tty.setraw(sys.stdin.fileno())
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
reverse = False
skipnext = False
quitit = False
searchstr = None
writeout('\x1b[2J\x1b[;H')
prepend = ''
try:
while not quitit:
if not skipnext:
newdata, delay = replay.get_output(reverse)
skipnext = False
reverse = False
if newdata:
if prepend:
newdata = prepend + newdata
prepend = b''
writeout(newdata)
newdata = ''
writeout('\x1b]0;[Time: {0}]\x07'.format(
time.strftime('%m/%d %H:%M:%S', time.localtime(replay.laststamp))))
try:
if hasattr(sys.stdout, 'buffer'):
sys.stdout.buffer.flush()
else:
sys.stdout.flush()
except IOError:
pass
while True:
select.select((sys.stdin,), (), (), 86400)
myinput = sys.stdin.read()
if myinput.startswith('\x1b[C') or myinput.startswith('\x1bOC') or myinput == '\r': # right
break
elif myinput.startswith('\x1b[D') or myinput.startswith('\x1bOD') or myinput == 'y': # left
writeout('\x1b[2J\x1b[;H')
reverse = True
break
elif myinput == 'G' or myinput.startswith('\x1b[F'):
replay.end()
reverse = True
break
elif myinput == 'g' or myinput.startswith('\x1b[H'):
replay.begin()
break
elif myinput.lower() == 'q' or myinput == '\x03':
quitit = True
break
elif myinput.lower() == 'd':
writeout('\x1b];{0}\x07'.format(replay.debuginfo()))
if hasattr(sys.stdout, 'buffer'):
sys.stdout.buffer.flush()
else:
sys.stdout.flush()
elif myinput.lower() == '/':
sys.stdout.write('\x1b7\x1b[99999;0H\x1b[2K')
searchstr = ''
nxtchr = '/'
while '\r' not in searchstr:
sys.stdout.write(nxtchr)
sys.stdout.flush()
select.select((sys.stdin,), (), (), 86400)
nxtchr = sys.stdin.read(1)
if nxtchr in ('\x08', '\x7f'):
searchstr = searchstr[:-1]
nxtchr = '\x08 \x08'
else:
searchstr += nxtchr
if not isinstance(searchstr, bytes):
searchstr = searchstr.encode('utf8')
searchstr = searchstr[:-1]
newdata, delay = replay.search(searchstr)
if not newdata:
sys.stdout.write('\x1b[1K\rNo match found')
prepend = b'\x1b[1K\x1b8'
else:
sys.stdout.write('\x1b[1K\x1b8')
sys.stdout.flush()
skipnext = True
break
elif myinput.lower() == 'n' and searchstr:
newdata, delay = replay.search(searchstr, True)
if not newdata:
sys.stdout.write('\x1b7\x1b[99999;0H\x1b[2KNo more matches found')
sys.stdout.flush()
prepend = b'\x1b[1K\x1b8'
skipnext = True
break
else:
pass # print(repr(myinput))
except Exception:
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK)
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
writeout('\x1b[m\x1b[?25h\n')
raise
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK)
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
writeout('\x1b[m\x1b[?25h\n')
def replay_to_console(txtfile):
if os.path.exists(txtfile + '.cbl'):
binfile = txtfile + '.cbl'
elif '.' not in txtfile:
if '/' not in txtfile:
txtfile = os.getcwd() + '/' + txtfile
sys.stderr.write('Unable to locate cbl file: "{0}"\n'.format(txtfile + '.cbl'))
sys.exit(1)
else:
fileparts = txtfile.split('.')
prefix = '.'.join(fileparts[:-1])
binfile = prefix + '.cbl.' + fileparts[-1]
if not os.path.exists(binfile):
sys.stderr.write('Unable to locate cbl file: "{0}"\n'.format(binfile))
sys.exit(1)
_replay_to_console(txtfile, binfile)
def dump_to_console(txtfile):
if os.path.exists(txtfile + '.cbl'):
binfile = txtfile + '.cbl'
elif '.' not in txtfile:
if '/' not in txtfile:
txtfile = os.getcwd() + '/' + txtfile
sys.stderr.write('Unable to locate cbl file: "{0}"\n'.format(txtfile + '.cbl'))
sys.exit(1)
else:
fileparts = txtfile.split('.')
prefix = '.'.join(fileparts[:-1])
binfile = prefix + '.cbl.' + fileparts[-1]
if not os.path.exists(binfile):
sys.stderr.write('Unable to locate cbl file: "{0}"\n'.format(binfile))
sys.exit(1)
replay = LogReplay(txtfile, binfile)
quitit = False
writeout('\x1b[2J\x1b[;H')
prepend = ''
try:
while not quitit:
newdata, delay = replay.get_output(False)
if newdata:
if prepend:
newdata = prepend + newdata
prepend = b''
writeout(newdata.replace(b'\r\n', '\r\n[ {0} ] '.format(time.strftime('%m/%d %H:%M:%S', time.localtime(replay.laststamp))).encode('utf8')))
newdata = ''
try:
if hasattr(sys.stdout, 'buffer'):
sys.stdout.buffer.flush()
else:
sys.stdout.flush()
except IOError:
pass
else:
break
except Exception:
writeout('\x1b[m\x1b[?25h\n')
raise
writeout('\x1b[m\x1b[?25h\n')
if __name__ == '__main__':
replay_to_console(sys.argv[1])
+18 -102
View File
@@ -16,55 +16,10 @@ import fcntl
import sys
import struct
import termios
import select
def get_screengeom(escfallback=False):
# returns height in cells, width in cells, width in pixels, height in pixels
geom = list(struct.unpack('hhhh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
b'........')))
if escfallback and (geom[0] == 0 or geom[1] == 0):
cellgeom = get_cell_geometry_using_esc_18t()
geom[0], geom[1] = cellgeom
if escfallback and (geom[2] == 0 or geom[3] == 0):
pixgeom = get_pixel_geometry_using_esc_14t()
geom[2], geom[3] = pixgeom
return geom
def get_pixel_geometry_using_esc_14t():
sys.stdout.write('\x1b[14t')
sys.stdout.flush()
rlist, _, _ = select.select([sys.stdin], [], [], 1)
if not rlist:
return 0, 0
response = ''
while True:
c = sys.stdin.read(1)
if c == 't':
break
response += c
if not response.startswith('\x1b[4;'):
return 0, 0
pixgeom = response[4:].split(';')
return int(pixgeom[1]), int(pixgeom[0])
def get_cell_geometry_using_esc_18t():
sys.stdout.write('\x1b[18t')
sys.stdout.flush()
rlist, _, _ = select.select([sys.stdin], [], [], 1)
if not rlist:
return 0, 0
response = ''
while True:
c = sys.stdin.read(1)
if c == 't':
break
response += c
if not response.startswith('\x1b[8;'):
return 0, 0
cellgeom = response[4:].split(';')
return int(cellgeom[0]), int(cellgeom[1])
def get_screenwidth():
return struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
b'....'))[1]
class ScreenPrinter(object):
def __init__(self, noderange, client, textlen=4):
@@ -74,9 +29,6 @@ class ScreenPrinter(object):
self.client = client
self.nodeoutput = {}
self.nodelist = []
self.nodepos = {}
self.lastrows = 0
self.fieldchanged = False
maxlen = 0
for ans in self.client.read('/noderange/{0}/nodes/'.format(noderange)):
if 'error' in ans:
@@ -92,68 +44,33 @@ class ScreenPrinter(object):
self.fieldwidth = maxlen + textlen + 1 # 1 for column
def set_output(self, node, text):
if self.nodeoutput.get(node, None) == text:
if self.nodeoutput[node] == text:
return
self.nodeoutput[node] = text
if len(text) >= self.textlen:
self.textlen = len(text) + 1
self.fieldwidth = self.textlen + self.nodenamelen + 1
self.fieldchanged = True
self.drawscreen(node)
self.drawscreen()
def drawscreen(self, node=None):
def drawscreen(self):
if self.squeeze:
currheight, currwidth, _, _ = get_screengeom()
currheight -= 2
if currheight < 1:
currheight = 1
currwidth = get_screenwidth()
numfields = currwidth // self.fieldwidth
fieldformat = '{{0:>{0}}}:{{1:{1}}}'.format(self.nodenamelen,
self.textlen)
#sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
if len(self.nodelist) < (numfields * currheight):
numfields = len(self.nodelist) // currheight + 1
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
else:
numfields = 1
fieldformat = '{0}: {1}'
if self.squeeze:
columns = [self.nodelist[x:x+currheight] for x in range(0, len(self.nodelist), currheight)]
if self.lastrows:
sys.stdout.write('\x1b[{0}A'.format(self.lastrows))
if node and self.lastrows == len(columns[0]) and not self.fieldchanged:
targline, targcol = self.nodepos[node]
if targline:
sys.stdout.write('\x1b[{0}B'.format(targline))
if targcol:
sys.stdout.write('\x1b[{0}C'.format(targcol))
sys.stdout.write(fieldformat.format(node, self.nodeoutput[node]))
sys.stdout.write('\r\x1b[{0}B'.format(self.lastrows - targline))
else:
self.lastrows = 0
self.fieldchanged = False
column = 0
for currow in range(0, len(columns[0])):
sys.stdout.write('\x1b[2K')
for col in columns:
try:
node = col[currow]
self.nodepos[node] = (currow, column)
sys.stdout.write(fieldformat.format(node, self.nodeoutput[node]))
column += len(fieldformat.format(node, self.nodeoutput[node]))
except IndexError:
break
sys.stdout.write('\n')
column = 0
self.lastrows += 1
sys.stdout.write('\x1b[J')
else:
if node:
nodes = [node]
else:
nodes = self.nodelist
for node in nodes:
sys.stdout.write(fieldformat.format(node, self.nodeoutput[node]))
currfields = 0
for node in self.nodelist:
if currfields >= numfields:
sys.stdout.write('\n')
currfields = 1
else:
currfields += 1
sys.stdout.write(fieldformat.format(node, self.nodeoutput[node]))
sys.stdout.write('\n')
sys.stdout.flush()
@@ -163,9 +80,8 @@ class ScreenPrinter(object):
if __name__ == '__main__':
import confluent.client as client
c = client.Command()
p = ScreenPrinter('d1-d12', c)
p.set_output('d3', 'Upload: 67%')
p.set_output('d7', 'Upload: 67%')
p = ScreenPrinter('n1-n13', c)
p.set_output('n3', 'Upload: 67%')
+4 -7
View File
@@ -53,16 +53,13 @@ class TermHandler(object):
connection = None
try:
connection, address = self.socket.accept()
connection.sendall(b"confetty control v1--\n")
connection.sendall("confetty control v1--\n")
cmd = connection.recv(8)
if b'GETWINID' == cmd:
winid = os.environ['WINDOWID']
if not isinstance(winid, bytes):
winid = winid.encode('utf8')
connection.sendall(winid)
if 'GETWINID' == cmd:
connection.sendall(os.environ['WINDOWID'])
connection.close()
except BaseException:
pass
finally:
if connection is not None:
connection.close()
connection.close()
+6 -15
View File
@@ -98,24 +98,17 @@ class GroupedData(object):
self.byoutput = {}
self.header = {}
self.client = confluentconnection
self.detectedpad = None
def generate_byoutput(self):
self.byoutput = {}
thepad = self.detectedpad if self.detectedpad else ''
for n in self.bynode:
output = ''
for ln in self.bynode[n]:
output += ln.replace(thepad, '', 1) + '\n'
output = '\n'.join(self.bynode[n])
if output not in self.byoutput:
self.byoutput[output] = set([n])
else:
self.byoutput[output].add(n)
def add_line(self, node, line):
wspc = re.search(r'^\s*', line).group()
if self.detectedpad is None or len(wspc) < len(self.detectedpad):
self.detectedpad = wspc
if node not in self.bynode:
self.bynode[node] = [line]
else:
@@ -138,6 +131,8 @@ class GroupedData(object):
def print_all(self, output=sys.stdout, skipmodal=False, reverse=False,
count=False):
self.generate_byoutput()
modaloutput = None
ismodal = True
if reverse:
outdatalist = sorted(
@@ -169,9 +164,7 @@ class GroupedData(object):
self.byoutput[outdata])))
currout += '\n====================================\n'
currout += outdata
if currout[-1] != '\n':
currout += '\n'
currout += '\n'
currout += '\n\n'
output.write(currout)
output.flush()
@@ -211,9 +204,7 @@ class GroupedData(object):
else:
currout += '\n'.join(colordiff(modaloutput.split('\n'),
outdata.split('\n')))
if currout[-1] != '\n':
currout += '\n'
currout += '\n'
currout += '\n\n'
if reverse:
revoutput.append(currout)
else:
@@ -228,4 +219,4 @@ if __name__ == '__main__':
if not line:
continue
groupoutput.add_line(*line.split(': ', 1))
groupoutput.print_deviants()
groupoutput.print_deviants()
+1 -1
View File
@@ -17,4 +17,4 @@
class Types(object):
text, json, filehandle = range(3)
text, json = range(2)
+13 -144
View File
@@ -15,15 +15,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import array
import ctypes
import ctypes.util
import confluent.tlv as tlv
import socket
import select
from datetime import datetime
import json
import os
import struct
try:
@@ -31,77 +25,6 @@ try:
except NameError:
unicode = str
try:
range = xrange
except NameError:
pass
class iovec(ctypes.Structure): # from uio.h
_fields_ = [('iov_base', ctypes.c_void_p),
('iov_len', ctypes.c_size_t)]
iovec_ptr = ctypes.POINTER(iovec)
class cmsghdr(ctypes.Structure): # also from bits/socket.h
_fields_ = [('cmsg_len', ctypes.c_size_t),
('cmsg_level', ctypes.c_int),
('cmsg_type', ctypes.c_int)]
@classmethod
def init_data(cls, cmsg_len, cmsg_level, cmsg_type, cmsg_data):
Data = ctypes.c_ubyte * ctypes.sizeof(cmsg_data)
class _flexhdr(ctypes.Structure):
_fields_ = cls._fields_ + [('cmsg_data', Data)]
datab = Data(*bytearray(cmsg_data))
return _flexhdr(cmsg_len=cmsg_len, cmsg_level=cmsg_level,
cmsg_type=cmsg_type, cmsg_data=datab)
def CMSG_LEN(length):
sizeof_cmshdr = ctypes.sizeof(cmsghdr)
return ctypes.c_size_t(CMSG_ALIGN(sizeof_cmshdr).value + length)
SCM_RIGHTS = 1
class msghdr(ctypes.Structure): # from bits/socket.h
_fields_ = [('msg_name', ctypes.c_void_p),
('msg_namelen', ctypes.c_uint),
('msg_iov', ctypes.POINTER(iovec)),
('msg_iovlen', ctypes.c_size_t),
('msg_control', ctypes.c_void_p),
('msg_controllen', ctypes.c_size_t),
('msg_flags', ctypes.c_int)]
def CMSG_ALIGN(length): # bits/socket.h
ret = (length + ctypes.sizeof(ctypes.c_size_t) - 1
& ~(ctypes.sizeof(ctypes.c_size_t) - 1))
return ctypes.c_size_t(ret)
def CMSG_SPACE(length): # bits/socket.h
ret = CMSG_ALIGN(length).value + CMSG_ALIGN(ctypes.sizeof(cmsghdr)).value
return ctypes.c_size_t(ret)
class ClientFile(object):
def __init__(self, name, mode, fd):
self.fileobject = os.fdopen(fd, mode)
self.filename = name
libc = ctypes.CDLL(ctypes.util.find_library('c'))
recvmsg = libc.recvmsg
recvmsg.argtypes = [ctypes.c_int, ctypes.POINTER(msghdr), ctypes.c_int]
recvmsg.restype = ctypes.c_int
sendmsg = libc.sendmsg
sendmsg.argtypes = [ctypes.c_int, ctypes.POINTER(msghdr), ctypes.c_int]
sendmsg.restype = ctypes.c_size_t
def decodestr(value):
ret = None
try:
@@ -111,13 +34,11 @@ def decodestr(value):
ret = value.decode('cp437')
except UnicodeDecodeError:
ret = value
except AttributeError:
return value
return ret
def unicode_dictvalues(dictdata):
for key in dictdata:
if isinstance(dictdata[key], bytes):
if isinstance(dictdata[key], str):
dictdata[key] = decodestr(dictdata[key])
elif isinstance(dictdata[key], datetime):
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
@@ -128,7 +49,7 @@ def unicode_dictvalues(dictdata):
def _unicode_list(currlist):
for i in range(len(currlist)):
for i in xrange(len(currlist)):
if isinstance(currlist[i], str):
currlist[i] = decodestr(currlist[i])
elif isinstance(currlist[i], dict):
@@ -137,13 +58,13 @@ def _unicode_list(currlist):
_unicode_list(currlist[i])
def send(handle, data, filehandle=None):
def send(handle, data):
if isinstance(data, unicode):
try:
data = data.encode('utf-8')
except AttributeError:
pass
if isinstance(data, bytes) or isinstance(data, unicode):
if isinstance(data, str) or isinstance(data, unicode):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
@@ -165,27 +86,9 @@ def send(handle, data, filehandle=None):
if tl > 16777215:
raise Exception("JSON data exceeds protocol limits")
# xor in the type (0b1 << 24)
if filehandle is None:
tl |= 16777216
handle.sendall(struct.pack("!I", tl))
handle.sendall(sdata)
else:
tl |= (2 << 24)
handle.sendall(struct.pack("!I", tl))
cdtype = ctypes.c_ubyte * len(sdata)
cdata = cdtype.from_buffer(bytearray(sdata))
ciov = iovec(iov_base=ctypes.addressof(cdata),
iov_len=ctypes.c_size_t(ctypes.sizeof(cdata)))
fd = ctypes.c_int(filehandle)
cmh = cmsghdr.init_data(
cmsg_len=CMSG_LEN(
ctypes.sizeof(fd)), cmsg_level=socket.SOL_SOCKET,
cmsg_type=SCM_RIGHTS, cmsg_data=fd)
mh = msghdr(msg_name=None, msg_len=0, msg_iov=iovec_ptr(ciov),
msg_iovlen=1, msg_control=ctypes.addressof(cmh),
msg_controllen=ctypes.c_size_t(ctypes.sizeof(cmh)))
sendmsg(handle.fileno(), mh, 0)
tl |= 16777216
handle.sendall(struct.pack("!I", tl))
handle.sendall(sdata)
def recvall(handle, size):
rd = handle.recv(size)
@@ -215,46 +118,12 @@ def recv(handle):
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
if dlen == 0:
return None
if datatype == tlv.Types.filehandle:
filehandles = array.array('i')
rawbuffer = bytearray(2048)
pkttype = ctypes.c_ubyte * 2048
data = pkttype.from_buffer(rawbuffer)
cmsgsize = CMSG_SPACE(ctypes.sizeof(ctypes.c_int)).value
cmsgarr = bytearray(cmsgsize)
cmtype = ctypes.c_ubyte * cmsgsize
cmsg = cmtype.from_buffer(cmsgarr)
cmsg.cmsg_level = socket.SOL_SOCKET
cmsg.cmsg_type = SCM_RIGHTS
cmsg.cmsg_len = CMSG_LEN(ctypes.sizeof(ctypes.c_int))
iov = iovec()
iov.iov_base = ctypes.addressof(data)
iov.iov_len = 2048
msg = msghdr()
msg.msg_iov = ctypes.pointer(iov)
msg.msg_iovlen = 1
msg.msg_control = ctypes.addressof(cmsg)
msg.msg_controllen = ctypes.sizeof(cmsg)
select.select([handle], [], [])
i = recvmsg(handle.fileno(), ctypes.pointer(msg), 0)
cdata = cmsgarr[CMSG_LEN(0).value:]
data = rawbuffer[:i]
if cmsg.cmsg_level == socket.SOL_SOCKET and cmsg.cmsg_type == SCM_RIGHTS:
try:
filehandles.fromstring(bytes(
cdata[:len(cdata) - len(cdata) % filehandles.itemsize]))
except AttributeError:
filehandles.frombytes(bytes(
cdata[:len(cdata) - len(cdata) % filehandles.itemsize]))
data = json.loads(bytes(data))
return ClientFile(data['filename'], data['mode'], filehandles[0])
else:
data = handle.recv(dlen)
while len(data) < dlen:
ndata = handle.recv(dlen - len(data))
if not ndata:
raise Exception("Error reading data")
data += ndata
data = handle.recv(dlen)
while len(data) < dlen:
ndata = handle.recv(dlen - len(data))
if not ndata:
raise Exception("Error reading data")
data += ndata
if datatype == tlv.Types.text:
return data
elif datatype == tlv.Types.json:
-346
View File
@@ -1,346 +0,0 @@
import asyncio
from PIL import Image
import io
import numpy as np
import queue
import threading
import time
import zlib
# This results in an RGBA organization of pixels
MYPIXFORMAT = bytearray([
32, # bits per pixel
24, # depth
0, # big endian
1, # true color
0, 255, # red max
0, 255, # green max
0, 255, # blue max
0, 8, 16, # red shift, green shift, blue shift
0, 0, 0 # padding
])
class ByteStream:
def __init__(self):
self.buffer = b''
def add_number(self, number, num_bytes):
data = number.to_bytes(num_bytes, byteorder='big', signed=True)
self.buffer += data
def extend(self, data):
self.buffer += data
def get_bytes(self):
return self.buffer
def clear(self):
self.buffer = b''
def flush(self, writer):
writer.write(self.buffer)
self.clear()
class VNCClient:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
return False
@classmethod
async def create(cls, url, outputfile=None, fps=15):
self = cls()
self.outputfile = outputfile
self.fps = fps
self.video_writer = None
self._video_size = None
self._last_frame = None
self._last_frame_time = None
self._video_queue = None
self._video_thread = None
self._cv2 = None
if outputfile:
try:
import cv2
except ImportError:
raise ImportError("OpenCV is required for video output but is not installed.")
self._cv2 = cv2
self._video_queue = queue.Queue()
self._video_thread = threading.Thread(
target=self._video_worker, daemon=True)
self._video_thread.start()
if url.startswith('unix://'):
url = url.replace('unix://', '')
if url.startswith('/'):
self.reader, self.writer = await asyncio.open_unix_connection(url)
elif url.startswith('@'):
url = '\0' + url[1:]
self.reader, self.writer = await asyncio.open_unix_connection(url)
elif url.startswith('tcp://'):
url = url.replace('tcp://', '')
host, port = url.split(':')
self.reader, self.writer = await asyncio.open_connection(host, int(port))
else:
raise ValueError('Unsupported URL: {}'.format(url))
self.receiver = None
self.framebuffer = None
self.copytext = None
self._updating = True
self.decompressor = zlib.decompressobj()
self._input_queue = asyncio.Queue()
self._input_task = asyncio.create_task(self._input_worker())
await self._do_vnc_handshake()
return self
async def _input_worker(self):
while True:
keys, modifierkeys = await self._input_queue.get()
payload = ByteStream()
for modkey in (modifierkeys or []):
payload.add_number(4, 1) # Key event
payload.add_number(1, 1) # Down
payload.add_number(0, 2) # Padding
payload.add_number(modkey.value, 4)
payload.flush(self.writer)
await self.writer.drain()
for key in keys:
keynumber = key.value if hasattr(key, 'value') else key
payload.add_number(4, 1) # Key event
payload.add_number(1, 1) # Down
payload.add_number(0, 2) # Padding
payload.add_number(keynumber, 4)
payload.add_number(4, 1) # Key event
payload.add_number(0, 1) # Up
payload.add_number(0, 2) # Padding
payload.add_number(keynumber, 4)
payload.flush(self.writer)
await self.writer.drain()
for modkey in (modifierkeys or []):
payload.add_number(4, 1) # Key event
payload.add_number(0, 1) # Up
payload.add_number(0, 2) # Padding
payload.add_number(modkey.value, 4)
payload.flush(self.writer)
await self.writer.drain()
await asyncio.sleep(0.01) # Have to slow down keypresses for some servers
# Still shouldn't be noticable interactively, but does slow down paste to a fast typist...
self._input_queue.task_done()
async def send_keypresses(self, keys, modifierkeys=None):
await self._input_queue.put((keys, modifierkeys))
async def _read_number(self, num_bytes):
data = await self.reader.readexactly(num_bytes)
return int.from_bytes(data, byteorder='big', signed=True)
def _write_number(self, number, num_bytes):
data = number.to_bytes(num_bytes, byteorder='big', signed=True)
self.writer.write(data)
return data
async def get_screenshot(self):
while self._updating:
await asyncio.sleep(0.1)
await asyncio.sleep(0)
if self.framebuffer is None:
raise Exception('No framebuffer data available')
self._updating = True
return self.framebuffer.copy()
async def _do_vnc_handshake(self):
rfbver = await self.reader.readline()
if not rfbver.startswith(b'RFB 003.008'):
self.writer.close()
await self.writer.wait_closed()
raise Exception('Unsupported RFB version')
self.writer.write(b'RFB 003.008\n')
numsectypes = await self._read_number(1)
if not numsectypes:
self.writer.close()
await self.writer.wait_closed()
raise Exception('No security types supported by the server')
sectypes = await self.reader.readexactly(numsectypes)
sectypes = bytearray(sectypes)
secresult = 1
if 1 in sectypes:
self.writer.write(b'\x01')
await self.writer.drain()
secresult = await self._read_number(4) # Security result
if secresult != 0:
self.writer.close()
await self.writer.wait_closed()
raise Exception('VNC authentication failed')
self.writer.write(b'\x01') # Share display
self.width = await self._read_number(2)
self.height = await self._read_number(2)
pixformat = await self.reader.readexactly(16)
name_length = await self._read_number(4)
self.name = await self.reader.readexactly(name_length)
payload = ByteStream()
if pixformat != MYPIXFORMAT:
payload.add_number(0, 1) # Set pixel format
payload.add_number(0, 3) # Padding
payload.extend(MYPIXFORMAT)
payload.flush(self.writer)
self.receiver = asyncio.create_task(self._receive_loop())
payload.add_number(2, 1) # Set encodings
payload.add_number(0, 1) # Padding
payload.add_number(4, 2) # Number of encodings
payload.add_number(6, 4) # zlib
payload.add_number(7, 4) # tight
payload.add_number(-223, 4) # desktopsize
payload.add_number(-308, 4) # extended desktopsize
payload.flush(self.writer)
self._request_screen_update(incremental=False)
def _request_screen_update(self, incremental=True):
incremental = 1 if incremental else 0
payload = ByteStream()
payload.add_number(3, 1) # Framebuffer update request
payload.add_number(incremental, 1) # Incremental
payload.add_number(0, 2) # x position
payload.add_number(0, 2) # y position
payload.add_number(self.width, 2) # width
payload.add_number(self.height, 2) # height
payload.flush(self.writer)
async def _receive_loop(self):
while True:
try:
message_type = await self._read_number(1)
if message_type == 0: # Framebuffer update
await self._handle_framebuffer_update()
elif message_type == 1: # Set color map entries
raise NotImplementedError('Set color map entries not implemented')
elif message_type == 2: # Bell
pass
elif message_type == 3: # Server cut text
padding = await self._read_number(3)
length = await self._read_number(4)
self.copytext = await self.reader.readexactly(length)
else:
raise Exception(f'Unknown message type: {message_type}')
except Exception as e:
print(f"Error in receive loop: {e}")
break
async def _handle_framebuffer_update(self):
_ = await self._read_number(1) # Padding
num_rects = await self._read_number(2)
self._updating = True
for _ in range(num_rects):
await self._handle_rectangle()
self._updating = False
self._write_video_frame()
self._request_screen_update(incremental=True)
def _write_video_frame(self):
if not self._cv2 or self.framebuffer is None:
return
# Snapshot the framebuffer now and hand it to the writer thread. Frames
# captured while a write is in progress simply queue up behind it.
frame = np.ascontiguousarray(
np.array(self.framebuffer.convert('RGB'))[:, :, ::-1])
self._video_queue.put((frame, time.monotonic()))
def _video_worker(self):
cv2 = self._cv2
while True:
frame, now = self._video_queue.get()
if frame is None:
# Flush the final frame for the time it stayed on screen
if self.video_writer is not None and self._last_frame is not None:
nframes = max(1, round(
(now - self._last_frame_time) * self.fps))
for _ in range(nframes):
self.video_writer.write(self._last_frame)
if self.video_writer is not None:
self.video_writer.release()
self.video_writer = None
return
if self.video_writer is None:
self._video_size = (frame.shape[1], frame.shape[0])
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
self.video_writer = cv2.VideoWriter(
self.outputfile, fourcc, self.fps, self._video_size)
if (frame.shape[1], frame.shape[0]) != self._video_size:
frame = cv2.resize(frame, self._video_size)
if self._last_frame is None:
self._last_frame = frame
self._last_frame_time = now
continue
# Hold the previous frame for the real time it was displayed
nframes = max(1, round((now - self._last_frame_time) * self.fps))
for _ in range(nframes):
self.video_writer.write(self._last_frame)
self._last_frame = frame
self._last_frame_time = now
async def _handle_rectangle(self):
if self.framebuffer is None:
self.framebuffer = Image.new('RGBA', (self.width, self.height))
x = await self._read_number(2)
y = await self._read_number(2)
width = await self._read_number(2)
height = await self._read_number(2)
encoding_type = await self._read_number(4)
pixel_data = None
if encoding_type == 6:
compressed_data_length = await self._read_number(4)
compressed_data = await self.reader.readexactly(compressed_data_length)
# Decompress the data using zlib and store it in the framebuffer
pixel_data = self.decompressor.decompress(compressed_data)
elif encoding_type == 0:
pixel_data = await self.reader.readexactly(width * height * 4) # Assuming 32 bits per pixel
if encoding_type in (-223, -308): # desktopsize
self.width = width
self.height = height
self.framebuffer = Image.new('RGBA', (self.width, self.height))
if encoding_type == -308:
nscreens = await self._read_number(1)
_ = await self._read_number(3) # padding
for _ in range(nscreens):
_ = await self.reader.readexactly(16) # screen info
elif pixel_data:
pixel_data = np.frombuffer(pixel_data, dtype=np.uint8).reshape((height, width, 4)).copy()
pixel_data[:, :, 3] = 0xff
img = Image.fromarray(pixel_data, 'RGBA')
self.framebuffer.paste(img, (x, y))
elif encoding_type == 7: # tight
# Best document I could see was:
# https://github.com/TurboVNC/tightvnc/blob/main/vnc_winsrc/rfb/rfbproto.h
tightheader = await self._read_number(1)
streamid = tightheader & 0x0F
if streamid:
raise NotImplementedError('tight encoding with streamid not implemented')
comptype = (tightheader >> 4) & 0x0F
if comptype != 9:
raise NotImplementedError(f'tight encoding with comptype {comptype} not implemented')
compressed_data_length = await self._read_tight_length()
compressed_data = await self.reader.readexactly(compressed_data_length)
with io.BytesIO(compressed_data) as jpgimg:
img = Image.open(jpgimg)
img.load()
self.framebuffer.paste(img, (x, y))
else:
raise Exception(f'Unsupported encoding type: {encoding_type}')
async def _read_tight_length(self):
length = 0
for i in range(3):
byte = await self._read_number(1)
length |= ((byte & 0x7F) << (i * 7))
if not (byte & 0x80):
break
return length
async def close(self):
if self._video_thread is not None:
# Signal the writer thread to flush and finalize the file
self._video_queue.put((None, time.monotonic()))
await asyncio.to_thread(self._video_thread.join)
self._video_thread = None
self.writer.close()
await self.writer.wait_closed()
+7 -35
View File
@@ -1,65 +1,37 @@
%define name confluent_client
%define version #VERSION#
%define fversion %{lua:
sv, _ = string.gsub("#VERSION#", "[~]", "-")
print(sv)
}
%define release 1
Summary: Client libraries and utilities for confluent
Name: %{name}
Version: %{version}
Release: %{release}
Source0: %{name}-%{fversion}.tar.gz
License: Apache-2.0
Source0: %{name}-%{version}.tar.gz
License: UNKNOWN
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Vendor: Lenovo
Url: http://github.com/lenovo/confluent
Vendor: Jarrod Johnson <jjohnson2@lenovo.com>
Url: http://xcat.sf.net/
Obsoletes: confluent_common
# SUSE keeps Pillow's upstream capitalisation and rpm capability names are
# case sensitive, so the lowercase EL spelling resolves to nothing there.
%if 0%{?suse_version}
Requires: python3-numpy python3-Pillow python3-matplotlib
%else
Requires: python3-numpy python3-pillow python3-matplotlib
%endif
%description
This package enables python development and command line access to
a confluent server.
%prep
%setup -n %{name}-%{fversion}
%setup -n %{name}-%{version} -n %{name}-%{version}
%build
%if "%{dist}" == ".el7"
python2 setup.py build
%else
python3 setup.py build
%endif
python setup.py build
%install
%if "%{dist}" == ".el7"
python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
%else
python3 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
%endif
python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
%if 0%{?suse_version}
# openSUSE patches setuptools to write a literal "#!python" placeholder into
# installed scripts and expects the packager to rewrite it. The stock
# %%python3_fix_shebang only looks at %%{_bindir}, so do it for our prefix.
sed -i '1s|^#!python|#!/usr/bin/python3|' $RPM_BUILD_ROOT/opt/confluent/bin/*
%endif
%clean
rm -rf $RPM_BUILD_ROOT
%files -f INSTALLED_FILES
%license /opt/confluent/share/licenses/confluent_client/LICENSE
%license /opt/confluent/share/licenses/confluent_client/COPYRIGHT
%defattr(-,root,root)
/opt/confluent
+36 -193
View File
@@ -19,8 +19,6 @@ alias nodebmcreset='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export
alias nodeboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeboot'
alias nodeconfig='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconfig'
alias nodeconsole='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconsole'
alias nodecertutil='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodecertutil'
alias nodedeploy='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodedeploy'
alias nodedefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodedefine'
alias nodeeventlog='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeeventlog'
alias nodefirmware='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodefirmware'
@@ -38,23 +36,20 @@ alias nodereseat='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export C
alias noderun='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderun'
alias nodesensors='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesensors'
alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesetboot'
alias nodestorage='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodestorage -i'
alias nodestorage='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodestorage'
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
alias nodelicense='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelicense'
# Do not continue for non-bash shells, the rest of this sets up bash completion functions
[ -z "$BASH_VERSION" -o -z "$PS1" ] && return
_confluent_get_args()
{
CMPARGS=($COMP_LINE)
NUMARGS=$((COMP_CWORD+1))
if [ "${COMP_WORDS[COMP_CWORD]}" == '' ]; then
NUMARGS=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
CMPARGS+=("")
fi
GENNED=""
# Whitespace separates candidate groups, while commas group synonyms.
# shellcheck disable=SC2068
for CAND in ${COMP_CANDIDATES[@]}; do
candarray=(${CAND//,/ })
matched=0
@@ -78,34 +73,23 @@ function _confluent_generic_completion()
{
_confluent_get_args
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
function _confluent_generic_ng_completion()
{
_confluent_get_args
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_ng_completion
return;
fi
}
_confluent_nodeidentify_completion()
{
COMP_CANDIDATES=("on,off,blink -h")
COMP_CANDIDATES=("on,off -h")
_confluent_generic_completion
}
_confluent_nodesetboot_completion()
{
COMP_CANDIDATES=("default,cd,network,setup,hd,floppy,usb -h -b -p")
COMP_CANDIDATES=("default,cd,network,setup,hd -h -b -p")
_confluent_generic_completion
}
@@ -125,7 +109,7 @@ _confluent_nodemedia_completion()
return
fi
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS -lt 3 ]; then
@@ -138,7 +122,7 @@ _confluent_nodefirmware_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[COMP_CWORD]}))
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[2]} == 'update' ]; then
@@ -152,84 +136,11 @@ _confluent_nodefirmware_completion()
fi
}
_confluent_osimage_completion()
{
_confluent_get_args
if [ $NUMARGS == 2 ]; then
COMPREPLY=($(compgen -W "initialize import importcheck updateboot rebase fetch" -- ${COMP_WORDS[COMP_CWORD]}))
return
elif [ ${CMPARGS[1]} == 'initialize' ]; then
COMPREPLY=($(compgen -W "-h -a -g -u -s -k -t -p -i -l -r" -- ${COMP_WORDS[COMP_CWORD]}))
elif [ ${CMPARGS[1]} == 'import' ] || [ ${CMPARGS[1]} == 'importcheck' ]; then
compopt -o default
COMPREPLY=()
return
elif [ ${CMPARGS[1]} == 'fetch' ]; then
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "$(osdeploy getfetchable)" -- "${COMP_WORDS[COMP_CWORD]}"))
return
fi
compopt -o dirnames
COMPREPLY=()
return
elif [ ${CMPARGS[1]} == 'updateboot' -o ${CMPARGS[1]} == 'rebase' ]; then
COMPREPLY=($(compgen -W "-n $(confetty show /deployment/profiles|sed -e 's/\///')" -- "${COMP_WORDS[COMP_CWORD]}"))
return
fi
}
_confluent_imgutil_completion()
{
_confluent_get_args
if [ $NUMARGS == 2 ]; then
COMPREPLY=($(compgen -W "build exec unpack pack capture" -- ${COMP_WORDS[COMP_CWORD]}))
return
elif [ ${CMPARGS[1]} == 'build' ]; then
COMPREPLY=($(compgen -W "-s $(ls /var/lib/confluent/distributions)" -- ${COMP_WORDS[COMP_CWORD]}))
return
elif [ ${CMPARGS[1]} == 'unpack' ]; then
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "-s $(ls /var/lib/confluent/public/os)" -- ${COMP_WORDS[COMP_CWORD]}))
return
fi
compopt -o dirnames
return
elif [ ${CMPARGS[1]} == 'pack' ]; then
if [ $NUMARGS == 3 ]; then
compopt -o dirnames
fi
COMPREPLY=()
return
elif [ ${CMPARGS[1]} == 'capture' ]; then
if [ $NUMARGS == 3 ]; then
_confluent_nn_completion
return
fi
return
elif [ ${CMPARGS[1]} == 'exec' ]; then
compopt -o dirnames
COMPREPLY=($(compgen -W "-v" -- ${COMP_WORDS[COMP_CWORD]}))
return
fi
}
_confluent_nodedeploy_completion()
{
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
COMPREPLY=($(compgen -W "-n $(confetty show /deployment/profiles|sed -e 's/\///')" -- "${COMP_WORDS[COMP_CWORD]}"))
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodeshell_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -c -- ${COMP_WORDS[COMP_CWORD]}))
COMPREPLY=($(compgen -c -- ${COMP_WORDS[-1]}))
return
fi
if [ $NUMARGS -gt 3 ]; then
@@ -247,7 +158,7 @@ _confluent_nodelicense_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "install list save delete" -- ${COMP_WORDS[COMP_CWORD]}))
COMPREPLY=($(compgen -W "install list" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'install' ]; then
@@ -255,11 +166,6 @@ _confluent_nodelicense_completion()
COMPREPLY=()
return
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'save' ]; then
compopt -o dirnames
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return
@@ -270,7 +176,7 @@ _confluent_nodesupport_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[COMP_CWORD]}))
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'servicedata' ]; then
@@ -290,95 +196,48 @@ _confluent_nodeattrib_completion()
_confluent_generic_completion
}
_confluent_define_completion()
{
COMP_CANDIDATES=$(nodegroupattrib 'everything' all | awk '{print $2}'|sed -e 's/://')
_confluent_get_args
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
fi
}
_confluent_nodegroupattrib_completion()
{
COMP_CANDIDATES=$(nodegroupattrib 'everything' all | awk '{print $2}'|sed -e 's/://')
_confluent_generic_ng_completion
}
_confluent_nodecertutil_completion()
{
_confluent_get_args
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return
fi
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "installbmccacert removebmccacert listbmccacerts signbmccert" -- ${COMP_WORDS[COMP_CWORD]}))
return
fi
if [ ${CMPARGS[2]} == 'installbmccacert' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ ${CMPARGS[2]} == 'signbmccert' ]; then
COMP_CANDIDATES=("--days --added-names")
_confluent_get_args
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
fi
}
_confluent_nn_completion()
{
_confluent_get_args
INPUT=${COMP_WORDS[COMP_CWORD]}
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
PREFIX=${COMP_WORDS[COMP_CWORD]}
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
_confluent_nodeconsole_completion()
{
_confluent_get_args
if [ ${CMPARGS[-2]} == '-a' ] || [ ${CMPARGS[-2]} == '--automation' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [[ ${COMP_WORDS[COMP_CWORD]} == -* ]]; then
COMPREPLY=($(compgen -W "-a --automation -e --headless -t --tile -l --log -T --Timestamp -s --screenshot -i --interval -v --video -w --windowed" -- "${COMP_WORDS[COMP_CWORD]}"))
return
fi
_confluent_nn_completion
}
_confluent_nr_completion()
{
CMPARGS=($COMP_LINE)
NUMARGS=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
fi
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
compopt -o default
COMPREPLY=()
return;
fi
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
PREFIX=${COMP_WORDS[COMP_CWORD]}
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
#COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
_confluent_ng_completion()
{
@@ -386,48 +245,32 @@ _confluent_ng_completion()
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
PREFIX=${COMP_WORDS[COMP_CWORD]}
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
_confluent_nodediscover_completion()
{
_confluent_get_args
if [ $NUMARGS == 2 ]; then
COMPREPLY=($(compgen -W "list assign rescan clear subscribe unsubscribe register" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
}
complete -F _confluent_nodediscover_completion nodediscover
complete -F _confluent_nodeattrib_completion nodeattrib
complete -F _confluent_nodeattrib_completion nodegroupattrib
complete -F _confluent_nr_completion nodebmcreset
complete -F _confluent_nodesetboot_completion nodeboot
complete -F _confluent_nr_completion nodeconfig
complete -F _confluent_nodeconsole_completion nodeconsole
complete -F _confluent_define_completion nodedefine
complete -F _confluent_define_completion nodegroupdefine
complete -F _confluent_nn_completion nodeconsole
complete -F _confluent_nr_completion nodeeventlog
complete -F _confluent_nodefirmware_completion nodefirmware
complete -F _confluent_nodedeploy_completion nodedeploy
complete -F _confluent_osimage_completion osdeploy
complete -F _confluent_imgutil_completion imgutil
complete -F _confluent_nodegroupattrib_completion nodegroupattrib
complete -F _confluent_ng_completion nodegroupattrib
complete -F _confluent_ng_completion nodegroupremove
complete -F _confluent_nr_completion nodehealth
complete -F _confluent_nodeidentify_completion nodeidentify
complete -F _confluent_nr_completion nodeinventory
complete -F _confluent_nodecertutil_completion nodecertutil
complete -F _confluent_nodeattrib_completion nodelist
complete -F _confluent_nr_completion nodelist
complete -F _confluent_nodemedia_completion nodemedia
complete -F _confluent_nodepower_completion nodepower
complete -F _confluent_nr_completion noderemove
+11 -22
View File
@@ -3,7 +3,7 @@ collate(1) -- Organize text input by node
## SYNOPSIS
`<other command> | collate [-a] [-b] [-d] [-w] [-g] [-s] [-c] [-r] [-l lognametemplate]`
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r] [-l lognametemplate]`
## DESCRIPTION
@@ -21,41 +21,30 @@ node and group names sorted alphanumerically.
when output differs along a telling group boundary. For example, if
output suggests a large number of nodes are unreachable, abbreviate
showing 'rack1' as being unreachable may make more obvious a possible cause.
* `-b BASE`, `--base=BASE`:
Use given node as reference for comparison when using
-d, instead of using the most common result
* `-d`, `--diff`:
Express all but the most common result group in terms of diff from
the most common result group
* `-l`, `--log`:
Save output per node to individual log files, replacing {node} in the name
with the nodename of each
* `-w`, `--watch`:
Update results dynamically as data becomes available, rather than
waiting for the command to fully complete.
* `-g`, `--groupcount`:
Show count of output groups rather than the actual
output
* `-s`, `--skipcommon`:
Suppress printing of the most common result text group. This is used to
focus on stray output against a well known and expected result.
* `-c`, `--count`:
Print a count of the number of nodes in an output group under the
noderange.
noderange.
* `-r`, `--reverse`:
Rather than starting with most common to least common, start with
the least common and print the most common last.
* `-l LOG`, `--log=LOG`:
Save output per node to individual log files, replacing {node} in the name
with the nodename of each
* `-h`, `--help`:
Show help message and exit
* `-r`, `--reverse`:
Rather than starting with most common to least common, start with
the least common and print the most common last.
## EXAMPLES
* Organizing power state of multiple nodes:
-4
View File
@@ -7,7 +7,6 @@ collective(1) -- Check and manage a confluent collective
`collective join <server> [-i TOKEN]`
`collective show`
`collective gencert`
`collective delete`
## DESCRIPTION
@@ -37,9 +36,6 @@ on circumstances.
* `-i`:
Provide the token as an argument rather than interactively.
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Inviting a server called mgt2:
+1 -16
View File
@@ -15,21 +15,6 @@ be mostly an aid for developing client software, with
day to day administration generally being easier with
the various function specific commands.
## OPTIONS
* `-s SERVER:PORT`, `--server=SERVER:PORT`:
Confluent instance to connect to
* `-c PATH`, `--control=PATH`:
Path to offer terminal control
* `-m MINTIME`, `--mintime=MINTIME`:
Minimum time to run or else pause for input (used to
keep a terminal from closing quickly on error)
* `-h`, `--help`:
Show help message and exit
## COMMANDS
The CLI may be navigated by shell commands and some other
@@ -42,7 +27,7 @@ commands.
* `show` **ELEMENT**, `cat` **ELEMENT**:
Display the result of reading a specific element (by full or relative path)
* `unset` **ELEMENT** **ATTRIBUTE**
For an element with attributes, request to clear the value of the attribute
For an element with attributes, request to clear the value of the attribue
* `set` **ELEMENT** **ATTRIBUTE**=**VALUE**
Set the specified attribute to the given value
* `start` **ELEMENT**
@@ -1,35 +0,0 @@
confluent2ansible(8) -- Export confluent node inventory to an Ansible hosts file
================================================================================
## SYNOPSIS
`confluent2ansible <noderange> -o <ansible.hosts>` [`-a`]
## DESCRIPTION
`confluent2ansible` reads the nodes matched by `<noderange>` from confluent and
writes a corresponding Ansible inventory (hosts) file, allowing an existing
confluent inventory to be used directly by Ansible.
## OPTIONS
* `-o FILE`, `--output=FILE`:
Write the Ansible hosts file to FILE.
* `-a`, `--append`:
Append to an existing hosts file rather than overwriting it.
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Write an Ansible hosts file for the compute group:
`# confluent2ansible compute -o /etc/ansible/hosts`
* Append a rack to an existing hosts file:
`# confluent2ansible rack3 -o /etc/ansible/hosts -a`
## SEE ALSO
confluent2xcat(8), confluent2lxca(8), nodelist(8)
@@ -1,243 +0,0 @@
confluent2dnsmasq(8) -- Generate dnsmasq static DHCP reservations for nodes
===============================================================================
## SYNOPSIS
`confluent2dnsmasq [options] <noderange>`
`confluent2dnsmasq <noderange>`
`confluent2dnsmasq --all-options <noderange>`
`confluent2dnsmasq -n <noderange>`
## DESCRIPTION
`confluent2dnsmasq` generates a dnsmasq configuration fragment of static
DHCP reservations for a noderange, using the confluent database as the source
of truth. Each `net.<network>.<attribute>` group is pulled together and one
`dhcp-host=` reservation is emitted for **every network that has a hardware
address** (`net.hwaddr`, `net.bmc.hwaddr`, `net.eth.mynetwork.hwaddr`, and so
on; the network component may itself contain several dotted parts). Networks
without a `hwaddr` (for example InfiniBand interfaces identified only by a
GUID) are skipped.
For each reservation, `net.<network>.ipv4_address` supplies the reserved
address. The `/prefixlen` suffix on that address is used to compute the
enclosing subnet; if the address carries no `/prefixlen`, the prefix length is
filled in from the matching directly-attached route in the host's routing
table. One `dhcp-range=<network>,static,<netmask>,<lease>` line is emitted per
distinct subnet. dnsmasq will not answer DHCP requests on a subnet that has no
covering `dhcp-range`, even when every address on it is statically reserved, so
these ranges are emitted by default (see `--no-range`). An address that has
neither an explicit prefix nor a matching local route still yields a
`dhcp-host` reservation, but no subnet or `dhcp-range` is derived for it and a
warning is printed.
The reservation name is the first token of `net.<network>.hostname`, falling
back to the node name itself for the primary (unnamed) network, and omitted for
a named network that has no hostname. Only IPv4 node addresses are used for
reservations and ranges.
By default the fragment begins with a `bind-dynamic` directive so that dnsmasq
can bind DHCP to networks confluent is using, too (see `--no-bind-dynamic`).
Also by default, a `listen-address` line is emitted for `127.0.0.1`, for
`::1`, and for this host's own address on each managed subnet (see
`--no-listen-address`). The per-subnet addresses are read from the routing
table of the host the command runs on, so run it on the dnsmasq host; only
subnets the host is directly attached to are added. Besides telling dnsmasq
where to listen, the presence of any `listen-address` line disables the default
`local-service` (or `local-service=host`) restriction in `dnsmasq.conf` that
would otherwise limit dnsmasq to localhost.
Additional data can optionally be added to the DHCP reply from the confluent
database (see OPTIONS; all are off by default). Each such option is scoped to
the subnet it belongs to using a dnsmasq tag: the `dhcp-host` lines for that
subnet are given a `set:<tag>` and the corresponding `dhcp-option` lines a
matching `tag:<tag>`. Values are aggregated per subnet; if two nodes on the
same subnet disagree (for example two different gateways), a warning is printed
and the first value is used.
The configuration is written to `/etc/dnsmasq.d/confluent-dhcp.conf` (override
with `--target`) and regenerated on each run, replacing the file atomically.
If the existing file was generated with different content-affecting arguments
(the preview and confirmation flags `-n` and `-y` are ignored for this
comparison), you are asked to confirm before it is overwritten; the prompt is
skipped with `-y` and is refused when there is no controlling terminal. If a
run produces no reservations at all (for example an empty noderange or a failed
read), `confluent2dnsmasq` refuses to overwrite the existing file unless
`--allow-empty` is given.
## OPTIONS
* `--target PATH`:
Path of the configuration file to write. Defaults to
`/etc/dnsmasq.d/confluent-dhcp.conf`.
* `--tag-prefix PREFIX`:
Prefix used when generating dnsmasq tag names for per-subnet options.
Defaults to `cdhcp`, yielding tags such as `cdhcp_10_28_104_0_21`.
* `--no-bind-dynamic`:
Do not emit the `bind-dynamic` line. It is emitted by default. Note that
`bind-dynamic` is a global dnsmasq directive and must not be combined with
a `bind-interfaces` directive set elsewhere. Without it, dnsmasq may conflict
with confluent's own DHCP unless `bind-dynamic` is set elsewhere in the
dnsmasq configuration; a warning is printed.
* `--no-listen-address`:
Do not autodetect or emit `listen-address` lines. By default a
`listen-address` line is emitted for `127.0.0.1`, for `::1`, and for this
host's address on each managed subnet. With this flag you must set
`interface`, `except-interface`, or `listen-address` yourself (or disable
`local-service`/`local-service=host` in `dnsmasq.conf`) for dnsmasq to serve
the cluster networks alongside confluent; a warning is printed as a reminder.
* `--no-range`:
Do not emit `dhcp-range` lines, leaving range and subnet declarations to be
managed elsewhere. Remember that dnsmasq will not serve DHCP on a subnet
that has no covering `dhcp-range`. Per-subnet options are still scoped via
host tags, so they keep working with a range you declare yourself.
* `--lease TIME`:
Lease time applied to the emitted `dhcp-range` lines. Defaults to `24h`.
Accepts any dnsmasq lease syntax (for example `1h`, `24h`, `infinite`).
Pass an empty string to omit the lease field entirely.
* `-n`, `--stdout`:
Write the configuration to standard output instead of the target file, which
is left untouched. Useful for previewing.
* `-y`, `--yes`:
Do not prompt for confirmation when the existing configuration was generated
with different settings; regenerate anyway. Without it a settings change is
confirmed interactively, and refused when there is no controlling terminal.
* `--allow-empty`:
Write the file even when no reservations were produced. By default an empty
result does not overwrite the existing file, to avoid discarding a good
configuration after an empty or failed read.
* `--all-options`:
Enable all of the optional reply-data options below at once.
* `--gateway`, `--router`:
Add a router (DHCP option 3) per subnet, taken from
`net.<network>.ipv4_gateway`.
* `--dns`:
Add DNS servers (DHCP option 6) per subnet, taken from `dns.servers`.
* `--domain`:
Add a domain name (DHCP option 15) per subnet, taken from `dns.domain`.
* `--ntp`:
Add NTP servers (DHCP option 42) per subnet, taken from `ntp.servers`.
* `--mtu`:
Add an interface MTU (DHCP option 26) per subnet, taken from
`net.<network>.mtu`.
* `-h`, `--help`:
Show a help message and exit.
## EXAMPLES
* Generate reservations for a noderange and write the default file:
`# confluent2dnsmasq everything`
* Preview the configuration for one node without writing anything:
`# confluent2dnsmasq -n node01`
* Include all optional reply data (gateway, DNS, domain, NTP, MTU):
`# confluent2dnsmasq --all-options everything`
* Include only the gateway, and use an eight hour lease:
`# confluent2dnsmasq --gateway --lease 8h everything`
* Regenerate non-interactively (for example from cron) after changing options, without the confirmation prompt:
`# confluent2dnsmasq --all-options -y everything`
* Emit only reservations (no ranges) to a custom file kept beside your own range and listen interface definitions:
`# confluent2dnsmasq --no-listen-address --no-range --target /etc/dnsmasq.d/confluent-reservations.conf everything`
## GENERATED CONFIGURATION
Given the following confluent attributes for node `node01`:
node01: net.hwaddr: 10:ff:e0:af:af:f5
node01: net.ipv4_address: 10.28.90.1/21
node01: net.ipv4_gateway: 10.28.88.1
node01: net.bmc.hwaddr: 10:ff:e0:a4:cf:b6
node01: net.bmc.hostname: node01-bmc
node01: net.bmc.ipv4_address: 10.28.106.1/21
node01: net.bmc.ipv4_gateway: 10.28.104.1
node01: net.ib0.hostname: node01-ib0
node01: net.ib0.ipv4_address: 10.28.101.1/21
`confluent2dnsmasq node01`, run on a dnsmasq host whose own addresses on the
two managed subnets are `10.28.88.250` and `10.28.104.250`, writes the file
below. The `ib0` network is skipped because it has no `hwaddr`; the two
remaining networks each yield a subnet (with its `dhcp-range`) and a
reservation, and a `listen-address` line is emitted for localhost and for this
host's address on each subnet:
# Managed by confluent2dnsmasq -- DO NOT EDIT BY HAND.
# Regenerate: confluent2dnsmasq node01
# Generated 2026-06-28 12:00:00 +0000
# Allow confluent and dnsmasq to share the same network for DHCP
bind-dynamic
# Listen on localhost plus this host's address on each managed subnet,
# so dnsmasq serves these networks alongside confluent and bind-dynamic
# (a listen-address line also overrides local-service in dnsmasq.conf).
listen-address=127.0.0.1
listen-address=::1
listen-address=10.28.88.250
listen-address=10.28.104.250
# subnet 10.28.88.0/21
dhcp-range=10.28.88.0,static,255.255.248.0,24h
dhcp-host=10:ff:e0:af:af:f5,10.28.90.1,node01
# subnet 10.28.104.0/21
dhcp-range=10.28.104.0,static,255.255.248.0,24h
dhcp-host=10:ff:e0:a4:cf:b6,10.28.106.1,node01-bmc
Adding `--gateway` scopes a router option to each subnet through a tag, and the
matching `dhcp-host` lines gain a `set:` tag so the option reaches them:
# subnet 10.28.88.0/21
dhcp-range=10.28.88.0,static,255.255.248.0,24h
dhcp-option=tag:cdhcp_10_28_88_0_21,option:router,10.28.88.1
dhcp-host=10:ff:e0:af:af:f5,set:cdhcp_10_28_88_0_21,10.28.90.1,node01
# subnet 10.28.104.0/21
dhcp-range=10.28.104.0,static,255.255.248.0,24h
dhcp-option=tag:cdhcp_10_28_104_0_21,option:router,10.28.104.1
dhcp-host=10:ff:e0:a4:cf:b6,set:cdhcp_10_28_104_0_21,10.28.106.1,node01-bmc
## NOTES
`bind-dynamic` is a global dnsmasq option; if it is set here it must not be
contradicted by a `bind-interfaces` directive elsewhere in the configuration.
The autodetected `listen-address` lines double as the mechanism that relaxes
dnsmasq's default `local-service`/`local-service=host` restriction (any
`interface`, `except-interface`, or `listen-address` line does), which is what
lets dnsmasq answer on the cluster networks instead of only localhost.
Detection only covers subnets the host running the command is directly attached
to, so run it there; with `--no-listen-address` you must supply `interface`,
`except-interface`, or `listen-address` yourself, or disable `local-service`.
After (re)generating the file, validate it with `dnsmasq --test` and restart
dnsmasq (for example `systemctl restart dnsmasq`); a reload (SIGHUP) does not
re-read `listen-address` or interface bindings.
## FILES
* `/etc/dnsmasq.d/confluent-dhcp.conf`:
Default output file (override with `--target`).
## SEE ALSO
nodeattrib(8), noderange(5), dnsmasq(8)
@@ -1,28 +0,0 @@
confluent2hosts(8) -- Generate /etc/hosts entries for nodes
========================================================
## SYNOPSIS
`confluent2hosts -i <ip expression> -n <name expression> <noderange>`
`confluent2hosts -a <noderange>`
## DESCRIPTION
`confluent2hosts` can be used to help generate /etc/hosts entries for a
noderange. There are two general approaches.
It can be used ad-hoc, using -i and -n to specify the address and name portions respectively. This accepts the standard confluent expression syntax, allowing for things like 172.30.1.{n1} or {node}.{dns.domain} or {bmc}.
It can also read from the confluent db, using `-a`. In this mode, each net.<value>.<attribute> group is pulled together into hosts lines. ipv4_address and ipv6_address fields are associated with the corresponding hostname attributes. You can use `-f` to put the FQDN first.
## EXAMPLES
* Generate /etc/hosts entries ad-hoc using default name:
`# confluent2hosts -i 10.2.3.{n1} d9-d12`
* Generate /etc/hosts entries ad-hoc using alternative name:
`# confluent2hosts -i 10.2.3.{n1} -n "{node}-alt {node}-alt.{dns.domain}" d9-d12`
* Generate /etc/hosts entries using the confluent DB as a reference:
`# confluent2hosts -a d9-d12`
@@ -1,30 +0,0 @@
confluent2lxca(8) -- Export confluent nodes to a Lenovo XClarity Administrator bulk import file
==============================================================================================
## SYNOPSIS
`confluent2lxca <noderange> -o <bulkimport.csv>`
## DESCRIPTION
`confluent2lxca` reads the nodes matched by `<noderange>` from confluent and
writes a CSV file suitable for bulk import into Lenovo XClarity Administrator
(LXCA), so that hardware already known to confluent can be brought under LXCA
management.
## OPTIONS
* `-o FILE`, `--output=FILE`:
Write the XClarity Administrator bulk import CSV to FILE.
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Generate a bulk import file for all nodes:
`# confluent2lxca everything -o bulkimport.csv`
## SEE ALSO
confluent2xcat(8), confluent2ansible(8), nodelist(8)
@@ -1,36 +0,0 @@
confluent2xcat(8) -- Export confluent nodes to an xCAT stanza definition
========================================================================
## SYNOPSIS
`confluent2xcat <noderange> -o <xcatnodes.def>` [`-m <macs.csv>`]
## DESCRIPTION
`confluent2xcat` reads the nodes matched by `<noderange>` from confluent and
writes an xCAT object definition stanza file, easing interoperation with or
migration to/from an xCAT environment. Optionally it can also write an xCAT
`macs.csv` file capturing the MAC addresses of the nodes.
## OPTIONS
* `-o FILE`, `--output=FILE`:
Write the xCAT stanza definition to FILE.
* `-m FILE`, `--macs=FILE`:
Write an xCAT macs.csv file to FILE.
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Export all nodes to an xCAT stanza file:
`# confluent2xcat everything -o xcatnodes.def`
* Also capture MAC addresses:
`# confluent2xcat everything -o xcatnodes.def -m macs.csv`
## SEE ALSO
confluent2ansible(8), confluent2lxca(8), nodelist(8)
+6 -73
View File
@@ -3,91 +3,24 @@ confluentdbutil(8) -- Backup or restore confluent database
## SYNOPSIS
`confluentdbutil [options] [dump|restore|merge] <path>`
`confluentdbutil [-u] [-v] showattrib <noderange> <attribute>...`
`confluentdbutil [options] <dump|restore> <path>`
## DESCRIPTION
**confluentdbutil** is a utility to export/import the confluent attributes
to/from json files. The path is a directory that holds the json version.
In order to perform restore, the confluent service must not be running. It
is required to indicate how the usernames/passwords are treated in
is required to indicate how to treat the usernames/passwords are treated in
the json files (password protected, removed from the files, or unprotected).
The `showattrib` subcommand prints the stored value of any attribute for the
given noderange, reading directly from the configuration store. Because it
does not talk to the confluent daemon, it can be used to read attribute values
even while confluent is stopped. It must be run directly on the confluent
server as root. By default, `secret.*` and `crypted.*` values are masked as
`********`, just as nodeattrib(8) shows them. With `-u`, they are revealed:
`secret.*` values decrypted to plaintext and `crypted.*` values as their
stored one-way hashes.
## OPTIONS
* `-p PASSWORD`, `--password=PASSWORD`:
* `-p`, `--password`:
If specified, information such as usernames and passwords will be encrypted
using the given password.
* `-i`, `--interactivepassword`:
Prompt for password.
* `-r`, `--redact`:
Indicates to replace usernames and passwords with a dummy string rather
than including them.
than included.
* `-u`, `--unprotected`:
With `dump`, the keys.json file will include the encryption keys without
any protection. With `showattrib`, show `secret.*` values decrypted to
plaintext and `crypted.*` values as their stored hashes rather than
masking them as `********`.
* `-s`, `--skipkeys`:
This specifies to dump the encrypted data without
dumping the keys needed to decrypt it. This is
suitable for an automated incremental backup, where an
earlier password protected dump has a protected
keys.json file, and only the protected data is needed.
Keys do not change and as such they do not require
incremental backup.
* `-x ATTRIBUTE`, `--exclude=ATTRIBUTE`:
Exclude matching node and node group attributes from `dump`, `restore`,
or `merge`.
The option may be specified multiple times. Attribute names may use
shell-style wildcards such as `net.*`.
A bare namespace such as `net` excludes all attributes below that namespace.
An exclusion omits the attribute from the data being written, it does not
preserve the value already in the database. A `restore` replaces the
database outright, so attributes excluded there are missing from the
restored configuration entirely; use `merge` to leave existing objects
untouched.
Node `groups`, node `id.index`, and node-group `noderange` are retained
so that restore can reconstruct node-group membership and preserve
node index assignments.
During merge, existing nodes and node groups are skipped as whole objects;
exclusions affect only new objects imported from the backup.
* `-y`, `--yaml`:
Use YAML instead of JSON as file format
* `-v`, `--value-only`:
With `showattrib`, print only the values, without node and attribute names
(useful for scripting).
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Show the decrypted BMC password confluent uses for a node (run on the
server as root):
`# confluentdbutil -u showattrib n1 secret.hardwaremanagementpassword`
`n1: secret.hardwaremanagementpassword: mypassword123`
* Get just the value, for use in a script:
`# confluentdbutil -u -v showattrib n1 secret.hardwaremanagementpassword`
`mypassword123`
* Dump configuration without (dynamic) deployment state:
`# confluentdbutil -u -x 'deployment.state*' dump /root/confluent-backup`
The keys.json file will include the encryption keys without any protection.
-41
View File
@@ -1,41 +0,0 @@
dir2img(8) -- Create a disk image from a directory for nodemedia upload
=======================================================================
## SYNOPSIS
`dir2img` [`-e <extra_mb>`] [`-s <total_mb>`] `<directory> <imagefile>` [`<label>`]
## DESCRIPTION
`dir2img` packages the contents of `<directory>` into a FAT-formatted "big
floppy" disk image written to `<imagefile>`. The resulting image is suitable
for upload with nodemedia(8) so that its files are presented to a node as
removable media. An optional volume `<label>` may be supplied.
By default the image is sized to fit the directory contents. The `-e` and `-s`
options allow reserving additional free space.
This command relies on `mtools` (specifically `mformat` and `mcopy`) being
installed.
## OPTIONS
* `-e EXTRA_MB`:
Reserve EXTRA_MB megabytes of free space in the image in addition to the
space required for the directory contents.
* `-s TOTAL_MB`:
Make the image TOTAL_MB megabytes in total, rather than sizing it to the
directory contents.
## EXAMPLES
* Create an image from a directory:
`# dir2img /var/tmp/drivers drivers.img`
* Create a 64 MB labelled image:
`# dir2img -s 64 /var/tmp/drivers drivers.img DRIVERS`
## SEE ALSO
nodemedia(8)
-158
View File
@@ -1,158 +0,0 @@
# imgutil(1) -- Work with confluent OS cloning and diskless images
## SYNOPSIS
`imgutil` `build` [<options>] <scratchdir>
`imgutil` `exec` [<options>] <scratchdir> [<cmd>...]
`imgutil` `unpack` <profilename> <scratchdir>
`imgutil` `pack` [<options>] <scratchdir> <profilename>
`imgutil` `capture` <node> <profilename>
## DESCRIPTION
**imgutil** is a utility for creating, managing, and deploying OS images for diskless boot and system cloning in a Confluent environment. It supports building images from scratch, capturing images from running systems, and packing/unpacking diskless profiles.
## COMMANDS
* `build`:
Build a new diskless image from scratch in the specified scratch directory.
* `exec`:
Start the specified scratch directory as a container and optionally run a command inside it.
* `unpack`:
Unpack a diskless image profile to a scratch directory for modification.
* `pack`:
Pack a scratch directory into a diskless profile that can be deployed.
* `capture`:
Capture an image for cloning from a running system.
## BUILD OPTIONS
* `-r`, `--addrepos` <repository>:
Repositories to add in addition to the main source. May be specified multiple times.
* `-p`, `--packagelist` <file>:
Filename of package list to replace default pkglist.
* `-a`, `--addpackagelist` <file>:
A list of additional packages to include. May be specified multiple times.
* `-s`, `--source` <directory>:
Directory to pull installation from, typically a subdirectory of `/var/lib/confluent/distributions`. By default, the repositories for the build system are used. For Ubuntu, this is not supported; the build system repositories are always used.
* `--arch` <architecture>:
Target architecture to build for (`x86_64` or `aarch64`). For Ubuntu, the
Debian-style names `amd64` and `arm64` are accepted as aliases. Building
for a foreign architecture is supported on EL and Ubuntu build hosts and
requires a statically linked qemu-user emulator registered in binfmt_misc
with the F (fix-binary) flag; on Ubuntu this is provided by the
qemu-user-static package (qemu-user-binfmt as of Ubuntu 26.04). For EL,
the architecture is also detected automatically when building from a `-s`
source tree.
* `-y`, `--non-interactive`:
Avoid prompting for confirmation.
* `-v`, `--volume` <mount>:
Directory to make available in the build environment. `-v /` will cause it to be mounted in image as `/run/external/`. `-v /:/run/root` will override the target to be `/run/root`. Something like `/var/lib/repository:-` will cause it to mount to the identical path inside the image. May be specified multiple times.
* <scratchdir>:
Directory to build new diskless root in.
## EXEC OPTIONS
* `-v`, `--volume` <mount>:
Directory to make available in the build environment. `-v /` will cause it to be mounted in image as `/run/external/`. `-v /:/run/root` will override the target to be `/run/root`. May be specified multiple times.
* <scratchdir>:
Directory of an unpacked diskless root.
* <cmd>:
Optional command to run (defaults to a shell).
## UNPACK OPTIONS
* <profilename>:
The diskless OS profile to unpack.
* <scratchdir>:
Directory to extract diskless root to.
## PACK OPTIONS
* `-b`, `--baseprofile` <profile>:
Profile to copy extra info from. For example, to make a new version of an existing profile, reference the previous one as baseprofile.
* `-u`, `--unencrypted`:
Pack an unencrypted image rather than encrypting.
* <scratchdir>:
Directory containing diskless root.
* <profilename>:
The desired diskless OS profile name to pack the root into.
## CAPTURE OPTIONS
* <node>:
Node to capture image from.
* <profilename>:
Profile name for captured image.
## EXAMPLES
Build a diskless image from a distribution:
imgutil build -s alma-9.6-x86_64 /tmp/myimage
Build an aarch64 EL diskless image on an x86_64 host (the architecture is
detected from the source tree):
imgutil build -s alma-9.6-aarch64 /tmp/myimage
Build an aarch64 Ubuntu diskless image on an amd64 Ubuntu host:
imgutil build --arch aarch64 /tmp/myimage
Execute a shell in an unpacked image:
imgutil exec /tmp/myimage
Execute a specific command in an image:
imgutil exec /tmp/myimage /bin/rpm -qa
Unpack an existing profile for modification:
imgutil unpack myprofile /tmp/myimage
Pack a modified image into a new profile:
imgutil pack /tmp/myimage myprofile-v2
Capture an image from a running node:
imgutil capture node01 production-image
## FILES
* `/var/lib/confluent/public/os/`:
Default location for OS profiles.
* `/var/lib/confluent/private/os/`:
Location for encrypted image keys and private data.
* `/var/lib/confluent/distributions/`:
Default location for installation sources.
## SEE ALSO
osdeploy(8)
## AUTHOR
Written for the Confluent project.
-30
View File
@@ -1,30 +0,0 @@
nodeapply(8) -- Execute command on many nodes in a noderange through ssh
=========================================================================
## SYNOPSIS
`nodeapply [options] <noderange>`
## DESCRIPTION
Provides shortcut access to a number of common operations against deployed
nodes. These operations include refreshing ssh certificates and configuration,
rerunning syncfiles, and executing specified postscripts.
## OPTIONS
* `-k`, `--security`
Refresh SSH configuration (hosts.equiv and node SSH certificates)
* `-F`, `--sync`
Rerun syncfiles from deployed profile
* `-P SCRIPTS`, `--scripts=SCRIPTS`
Re-run specified scripts, with full path under scripts specified, e.g. post.d/scriptname,firstboot.d/otherscriptname
* `-c COUNT`, `-f COUNT`, `--count=COUNT`
Specify the maximum number of instances to run concurrently
* `-m MAXNODES`, `--maxnodes=MAXNODES`
Specify a maximum number of nodes to run remote ssh command to, prompting
if over the threshold
+102
View File
@@ -0,0 +1,102 @@
nodeattrib(8) -- List or change confluent nodes attributes
=========================================================
## SYNOPSIS
`nodeattrib [-b] <noderange> [all|<nodeattribute>...]`
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
**nodeattrib** manages the attributes of confluent nodes. In
the simplest form, it simply takes the given noderange(5) and lists the
matching nodes, one line at a time.
If a list of node attribute names are given, the value of those are also
displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. Attributes can be
straightforward values, or an expression as documented in nodeattribexpressions(5).
For a full list of attributes, run `nodeattrib <node> all` against a node.
If `-c` is specified, this will set the nodeattribute to a null value.
This is different from setting the value to an empty string.
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
If the word all is specified, then all available attributes are given.
Omitting any attribute name or the word 'all' will display only attributes
that are currently set.
For the `groups` attribute, it is possible to add a group by doing
`groups,=<newgroup>` and to remove by doing `groups^=<oldgroup>`
Note that `nodeattrib <group>` will likely not provide the expected behavior.
See nodegroupattrib(8) command on how to manage attributes on a group level.
## OPTIONS
* `-b`, `--blame`:
Annotate inherited and expression based attributes to show their base value.
* `-c`, `--clear`:
Clear specified nodeattributes
* `-e`, `--environment`:
Set specified attributes based on exported environment variable of matching name. Environment variable names may be lower case or all upper case. Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE
* `-p`, `--prompt`:
Request interactive prompting to provide values rather than the command line or environment variables.
## EXAMPLES
* Listing matching nodes of a simple noderange:
`# nodeattrib n1-n2`
`n1: console.method: ipmi`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: ipmi`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting an attribute of nodes matching a noderange:
`# nodeattrib n1,n2 hardwaremanagement.manager`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting a group of attributes while determining what group defines them:
`# nodeattrib n1,n2 hardwaremanagement --blame`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
`n1: hardwaremanagement.switch: r8e1`
`n1: hardwaremanagement.switchport: 14`
`n2: hardwaremanagement.manager: 172.30.3.2`
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
`n2: hardwaremanagement.switch: r8e1`
`n2: hardwaremanagement.switchport: 2`
* Listing matching nodes of a simple noderange that are set:
`# nodeattrib n1-n2 current`
`n1: console.method: ipmi`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: ipmi`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Change attribute on nodes of a simple noderange:
`# nodeattrib n1-n2 console.method=serial`
`n1: console.method: serial`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: serial`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Clear attribute on nodes of a simple noderange, if you want to retain the variable set the attribute to "":
`# nodeattrib n1-n2 -c console.method`
`# nodeattrib n1-n2 console.method`
`n1: console.method: `
`n2: console.method: `
* List all switches that a node is described as connected to:
`# nodeattrib d1 net.*switch`
`d1: net.mgt.switch: mgtswitch1`
`d1: net.pxe.switch: pxeswitch1`
`d1: net.switch:`
## SEE ALSO
nodegroupattrib(8), nodeattribexpressions(5)
@@ -1,183 +0,0 @@
nodeattrib(8) -- List or change confluent nodes attributes
=========================================================
## SYNOPSIS
`nodeattrib [-b] <noderange> [all|<nodeattribute>...]`
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib <noderange> -s <attributes.batch> ...`
## DESCRIPTION
**nodeattrib** manages the attributes of confluent nodes. In
the simplest form, it simply takes the given noderange(5) and lists the
matching nodes, one line at a time.
If a list of node attribute names are given, the value of those are also
displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. Attributes can be
straightforward values, or an expression as documented in nodeattribexpressions(5).
For a full list of attributes, run `nodeattrib <node> all` against a node.
If `-c` is specified, this will set the nodeattribute to a null value.
This is different from setting the value to an empty string.
Arbitrary custom attributes can also be created with the `custom.` prefix.
Network attributes (`net.*`) may similarly be qualified with an arbitrary name, or dotted chain of names, between
`net` and the attribute, for example `net.compute.ipv4_address` or `net.eth.compute.ipv4_address` and
`net.eth.management.ipv4_address`. Any such `net.<name*>.*` attributes together describe one logical network
interface, so custom networks may be defined freely by choosing your own name(s).
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
If the word all is specified, then all available attributes are given.
Omitting any attribute name or the word 'all' will display only attributes
that are currently set.
The values of `secret.*` and `crypted.*` attributes are never shown here; they
can only be retrieved on the confluent server with
`confluentdbutil -u showattrib`.
For the `groups` attribute, it is possible to add a group by doing
`groups,=<newgroup>` and to remove by doing `groups^=<oldgroup>`
Note that `nodeattrib <group>` will likely not provide the expected behavior.
See the nodegroupattrib(8) command for how to manage attributes on a group level. Running
nodeattrib on a group will simply set node-specific attributes on each individual
member of the group.
Additionally, if wanting to change from a node level attribute value to inherit from a
member group, it is required to use '-c' to clear the attribute. Conversely, assigning
to a blank value will allow masking a group defined attribute with an empty value.
## OPTIONS
* `-b`, `--blame`:
Annotate inherited and expression based attributes to show their base value.
* `-c`, `--clear`:
Clear specified nodeattributes.
* `-e`, `--environment`:
Set specified attributes based on exported environment variable of matching name.
Environment variable names may be lower case or all upper case.
Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE)
* `-p`, `--prompt`:
Request interactive prompting to provide values rather than the command line
or environment variables.
* `-s`, `--set`:
Set attributes using a batch file rather than the command line. The attributes in the batch file
can be specified as one line of key=value pairs similar to command line or each attribute can
be in its own line. Lines that start with # sign will be read as a comment. See EXAMPLES for batch
file syntax.
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
Prompt if trying to set attributes on more than
specified number of nodes.
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Listing matching nodes of a simple noderange:
`# nodeattrib n1-n2`
`n1: console.method: ipmi`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: ipmi`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting an attribute of nodes matching a noderange:
`# nodeattrib n1,n2 hardwaremanagement.manager`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting a group of attributes while determining what group defines them:
`# nodeattrib n1,n2 hardwaremanagement --blame`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
`n1: net.switch: r8e1`
`n1: net.switchport: 14`
`n2: hardwaremanagement.manager: 172.30.3.2`
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
`n2: net.switch: r8e1`
`n2: net.switchport: 2`
* Listing matching nodes of a simple noderange that are set:
`# nodeattrib n1-n2 current`
`n1: console.method: ipmi`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: ipmi`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Change attribute on nodes of a simple noderange:
`# nodeattrib n1-n2 console.method=serial`
`n1: console.method: serial`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: serial`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Setting up a named network interface as a bond (e.g. a two-port LACP bond used for compute traffic), using an
expression to calculate the IP address:
`# nodeattrib node12 net.compute.team_mode=lacp net.compute.connection_name=bond0 net.compute.interface_names=enp33s0f0np0,enp33s0f1np1 net.compute.ipv4_address='172.17.0.{n1}/16'`
`node12: net.compute.connection_name: bond0`
`node12: net.compute.interface_names: enp33s0f0np0,enp33s0f1np1`
`node12: net.compute.ipv4_address: 172.17.0.12/16`
`node12: net.compute.team_mode: lacp`
* Passing additional settings to the network backend of the deployed OS with `net.extra_settings`
(semicolon-delimited key=value pairs, keys in the native syntax of the respective backend).
On a NetworkManager based OS (e.g. Enterprise Linux), keys are nmcli properties:
`# nodeattrib node12 net.mgmt.extra_settings='connection.zone=internal;ipv4.routes=10.0.0.0/8 192.168.1.254, 172.16.0.0/12 192.168.1.254;ipv4.route-metric=200'`
`node12: net.mgmt.extra_settings: connection.zone=internal;ipv4.routes=10.0.0.0/8 192.168.1.254, 172.16.0.0/12 192.168.1.254;ipv4.route-metric=200`
* On a netplan based OS (e.g. Ubuntu), keys are netplan YAML paths (nested keys dotted, values in YAML flow syntax).
Since Confluent uses braces for attribute expressions, literal braces must be escaped as `{{` and `}}` when setting the attribute:
`# nodeattrib node13 net.mgmt.extra_settings='routes=[{{to: 10.0.0.0/8, via: 192.168.1.254}}];nameservers.search=[lab.example.com]'`
`node13: net.mgmt.extra_settings: routes=[{to: 10.0.0.0/8, via: 192.168.1.254}];nameservers.search=[lab.example.com]`
* On a wicked based OS (e.g. SUSE), keys are ifcfg variables (routes are not supported through this mechanism on wicked):
`# nodeattrib node14 net.mgmt.extra_settings='ZONE=internal;ETHTOOL_OPTIONS=-K iface tso off'`
`node14: net.mgmt.extra_settings: ZONE=internal;ETHTOOL_OPTIONS=-K iface tso off`
* Clear attribute on nodes of a simple noderange, if you want to retain the variable set the attribute to "":
`# nodeattrib n1-n2 -c console.method`
`# nodeattrib n1-n2 console.method`
`n1: console.method: `
`n2: console.method: `
* List all switches that a node is described as connected to:
`# nodeattrib d1 net.*switch`
`d1: net.mgt.switch: mgtswitch1`
`d1: net.pxe.switch: pxeswitch1`
`d1: net.switch:`
* Setting attributes using a batch file with syntax similar to command line:
`# cat nodeattributes.batch`
`# power`
`power.psu1.outlet=3 power.psu1.pdu=pdu2`
`# nodeattrib n41 -s nodeattributes.batch`
`n41: 3`
`n41: pdu2`
* Setting attributes using a batch file with syntax where each attribute is in its own line:
`# cat nodeattributes.batch`
`# management`
`custom.mgt.switch=switch_main`
`custom.mgt.switch.port=swp4`
`# nodeattrib n41 -s nodeattributes.batch`
`n41: switch_main`
`n41: swp4`
## SEE ALSO
nodegroupattrib(8), nodeattribexpressions(5)
## ATTRIBUTES
@@ -44,7 +44,7 @@ It is sometimes the case that the number must be formatted a different way,
either specifying 0 padding or converting to hexadecimal. This can be done by a
number of operators at the end to indicate formatting changes.
`{n1:02x} - Zero pad to two decimal places, and convert to hexadecimal, as might be used for generating MAC addresses`
`{n1:02x} - Zero pad to two decimal places, and convert to hexadecimal, as mightbe used for generating MAC addresses`
`{n1:x} - Hexadecimal without padding, as may be used in a generated IPv6 address`
`{n1:X} - Uppercase hexadecimal`
`{n1:02d} - Zero pad a normal numeric representation of the number.`
@@ -1,28 +0,0 @@
nodebmcpassword(8) -- Change management controller password for a specified user
=========================================================
## SYNOPSIS
`nodebmcpassword <noderange> <username> <new_password>`
## DESCRIPTION
`nodebmcpassword` allows you to change the management controller password for a user on a specified noderange
## OPTIONS
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
Number of nodes to affect before prompting for
confirmation
* `-h`, `--help`:
Show help message and exit
## EXAMPLES:
* Change the management controller password for user USERID on nodes n1 through n4:
`# nodebmcpassword n1-n4 USERID newp4ssw0rd`
`n1: Password Change Successful`
`n2: Password Change Successful`
`n3: Password Change Successful`
`n4: Password Change Successful`

Some files were not shown because too many files have changed in this diff Show More