mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 00:31:09 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 123a002b6c | |||
| 2721f2bd7b | |||
| 43f26d8cbb |
@@ -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"
|
||||
@@ -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
@@ -1,7 +1 @@
|
||||
*.pyc
|
||||
.*.
|
||||
confluent_client/man/man*
|
||||
.vscode
|
||||
.*.sw*
|
||||
.sw*
|
||||
.idea/*
|
||||
.idea
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Confluent
|
||||
|
||||
 [](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,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"]
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
for package in /srpms/*; do
|
||||
rpmbuild --rebuild $package
|
||||
done
|
||||
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
%_gpg_digest_algo sha256
|
||||
%_gpg_name Lenovo Scalable Infrastructure
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
for package in /srpms/*; do
|
||||
rpmbuild --rebuild $package
|
||||
done
|
||||
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
%_gpg_digest_algo sha256
|
||||
%_gpg_name Lenovo Scalable Infrastructure
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
for package in /srpms/*; do
|
||||
rpmbuild --rebuild $package
|
||||
done
|
||||
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
%_gpg_digest_algo sha256
|
||||
%_gpg_name Lenovo Scalable Infrastructure
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Origin: Lenovo HPC Packages
|
||||
Label: Lenovo HPC Packages
|
||||
Codename: #CODENAME#
|
||||
Architectures: amd64 source
|
||||
Components: main
|
||||
Description: Lenovo HPC Packages
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Origin: Lenovo HPC Packages
|
||||
Label: Lenovo HPC Packages
|
||||
Codename: #CODENAME#
|
||||
Architectures: amd64 source
|
||||
Components: main
|
||||
Description: Lenovo HPC Packages
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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,4 +1,4 @@
|
||||
#!/usr/bin/python2
|
||||
#!/usr/bin/env python
|
||||
import csv
|
||||
import optparse
|
||||
import signal
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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/* ::
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+20
-1339
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -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:]))
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,4 +1,4 @@
|
||||
#!/usr/bin/python2
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
#!/usr/bin/python2
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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,4 +1,4 @@
|
||||
#!/usr/bin/python2
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2019 Lenovo
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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()
|
||||
@@ -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('')
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
@@ -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%')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -17,4 +17,4 @@
|
||||
|
||||
|
||||
class Types(object):
|
||||
text, json, filehandle = range(3)
|
||||
text, json = range(2)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user