mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 00:31:09 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce79c4cf11 |
@@ -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,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,8 +1,6 @@
|
||||
Name: confluent-client
|
||||
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
|
||||
@@ -13,8 +11,7 @@ Copyright: 2014 IBM Corporation
|
||||
2015-2019 Lenovo
|
||||
License: Apache-2.0
|
||||
|
||||
File: sortutil.py,
|
||||
tlvdata.py
|
||||
File: sortutil.py
|
||||
Copyright: 2014 IBM Corporation
|
||||
2015-2016 Lenovo
|
||||
License: Apache-2.0
|
||||
@@ -22,56 +19,3 @@ 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
|
||||
@@ -14,16 +14,11 @@ import shutil
|
||||
|
||||
shutil.copyfile('doc/man/nodeattrib.ronn.tmpl', 'doc/man/nodeattrib.ronn')
|
||||
shutil.copyfile('doc/man/nodegroupattrib.ronn.tmpl', 'doc/man/nodegroupattrib.ronn')
|
||||
with open('doc/man/nodeattrib.ronn', 'a') as outf:
|
||||
for field in sorted(attr.node):
|
||||
outf.write('\n* `{0}`:\n {1}\n'.format(field, attr.node[field]['description']))
|
||||
with open('doc/man/nodegroupattrib.ronn', 'a') as outf:
|
||||
for field in sorted(attr.node):
|
||||
outf.write('\n* `{0}`:\n {1}\n'.format(field, attr.node[field]['description']))
|
||||
|
||||
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')
|
||||
|
||||
@@ -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'))
|
||||
@@ -52,8 +50,6 @@ argparser.add_option('-s', '--skipcommon', action='store_true',
|
||||
'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 +67,13 @@ 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,7 +84,6 @@ fullline = sys.stdin.readline()
|
||||
printpending = True
|
||||
clearpending = False
|
||||
holdoff = 0
|
||||
padded = None
|
||||
while fullline:
|
||||
for line in fullline.split('\n'):
|
||||
if not line:
|
||||
@@ -114,18 +92,13 @@ while fullline:
|
||||
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)
|
||||
output = output.lstrip()
|
||||
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)
|
||||
output = output.lstrip()
|
||||
grouped.add_line(node, output)
|
||||
if options.watch:
|
||||
if not holdoff:
|
||||
@@ -145,5 +118,5 @@ while fullline:
|
||||
if printpending:
|
||||
if clearpending:
|
||||
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
|
||||
print_current(options.tabular)
|
||||
print_current()
|
||||
|
||||
|
||||
+33
-267
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# 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
|
||||
@@ -107,8 +107,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 +130,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 +149,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 +170,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()
|
||||
|
||||
|
||||
@@ -461,14 +450,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:]:
|
||||
@@ -624,7 +611,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,19 +623,17 @@ 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)
|
||||
if oldtcattr is not None:
|
||||
@@ -669,18 +654,13 @@ 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):
|
||||
@@ -859,20 +839,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 +846,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():
|
||||
@@ -990,7 +904,6 @@ def main():
|
||||
# sys.stdout.write('\x1b[H\x1b[J')
|
||||
# sys.stdout.flush()
|
||||
global powerstate, powertime, clearpowermessage
|
||||
|
||||
|
||||
|
||||
if sys.stdout.isatty():
|
||||
@@ -1004,13 +917,10 @@ 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)
|
||||
@@ -1021,13 +931,9 @@ 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:
|
||||
@@ -1042,9 +948,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,81 +964,8 @@ 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:
|
||||
@@ -1142,84 +974,20 @@ def consume_termdata(fh, bufferonly=False):
|
||||
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'
|
||||
data = client.stringify(data)
|
||||
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
|
||||
try:
|
||||
sys.stdout.write(data)
|
||||
except UnicodeEncodeError:
|
||||
sys.stdout.buffer.write(data.encode('utf8'))
|
||||
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):
|
||||
@@ -1233,8 +1001,6 @@ def consume_termdata(fh, bufferonly=False):
|
||||
# this scenario comfortable that it
|
||||
# will come out soon enough
|
||||
pass
|
||||
if shouldexit:
|
||||
quitconfetty(fullexit=True)
|
||||
else:
|
||||
deadline = 5
|
||||
connected = False
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
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()
|
||||
@@ -3,7 +3,6 @@ import argparse
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import shutil
|
||||
import sys
|
||||
try:
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
@@ -15,6 +14,7 @@ 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 partitionhostsline(line):
|
||||
comment = ''
|
||||
@@ -33,8 +33,6 @@ class HostMerger(object):
|
||||
self.byip = {}
|
||||
self.byname = {}
|
||||
self.byname6 = {}
|
||||
self.ipbyname = {}
|
||||
self.ipbyname6 = {}
|
||||
self.sourcelines = []
|
||||
self.targlines = []
|
||||
|
||||
@@ -56,21 +54,14 @@ class HostMerger(object):
|
||||
|
||||
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
|
||||
|
||||
@@ -127,7 +118,6 @@ def main():
|
||||
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:
|
||||
@@ -167,7 +157,7 @@ def main():
|
||||
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)
|
||||
namesbynode[node][currnet] = re.split('\s+|,', val)
|
||||
for node in ip4bynode:
|
||||
mydomain = domainsbynode.get(node, None)
|
||||
for ipdb in (ip4bynode, ip6bynode):
|
||||
@@ -183,15 +173,10 @@ def main():
|
||||
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.append('{0}.{1}'.format(name, mydomain))
|
||||
names = ' '.join(names)
|
||||
merger.add_entry(ipdb[node][currnet], names)
|
||||
merger.write_out('/etc/whatnowhosts')
|
||||
else:
|
||||
namesbynode = {}
|
||||
ipbynode = {}
|
||||
@@ -206,7 +191,7 @@ def main():
|
||||
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')
|
||||
os.rename('/etc/hosts', '/etc/hosts.confluentbkup')
|
||||
merger.write_out('/etc/hosts')
|
||||
|
||||
|
||||
|
||||
@@ -8,35 +8,30 @@ 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]):
|
||||
def create_image(directory, image, label=None):
|
||||
ents = 0
|
||||
datasz = 512
|
||||
for dir in os.walk(sys.argv[1]):
|
||||
ents += 1
|
||||
for filen in dir[2]:
|
||||
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
|
||||
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 // 16384 + 1
|
||||
with open(image, 'wb') as imgfile:
|
||||
imgfile.seek(datasz * 65536 - 1)
|
||||
imgfile.seek(datasz * 16384 - 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', '::'])
|
||||
'-s', '16','-h', '2', '::'])
|
||||
else:
|
||||
subprocess.check_call(['mformat', '-i', image, '-r', '16', '-d', '1', '-t',
|
||||
str(datasz), '-s', '32','-h', '4', '::'])
|
||||
str(datasz), '-s', '16','-h', '2', '::'])
|
||||
# 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
|
||||
@@ -57,25 +52,10 @@ def create_image(directory, image, label=None, esize=0, totalsize=None):
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 3:
|
||||
sys.stderr.write("Usage: {0} <directory> <imagefile>\n".format(
|
||||
sys.stderr.write("Usage: {0} <directory> <imagefile>".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 len(sys.argv) > 3:
|
||||
label = sys.argv[3]
|
||||
create_image(sys.argv[1], sys.argv[2], label)
|
||||
|
||||
@@ -36,38 +36,6 @@ 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')
|
||||
@@ -83,8 +51,6 @@ def run():
|
||||
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, '
|
||||
@@ -102,22 +68,21 @@ def run():
|
||||
|
||||
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 = []
|
||||
|
||||
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)
|
||||
|
||||
cmdstorun = []
|
||||
if options.security:
|
||||
@@ -128,17 +93,8 @@ def run():
|
||||
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:
|
||||
@@ -146,23 +102,19 @@ def run():
|
||||
cmdv = ['ssh', sshnode] + cmdvbase + cmdstorun[0]
|
||||
if currprocs < concurrentprocs:
|
||||
currprocs += 1
|
||||
run_cmdv(sshnode, cmdv, all, poller, pipedesc)
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
else:
|
||||
pendingexecs.append((sshnode, cmdv))
|
||||
pendingexecs.append((node, cmdv))
|
||||
if not all or exitcode:
|
||||
sys.exit(exitcode | autoexitcode)
|
||||
rdy = poller.poll(10)
|
||||
sys.exit(exitcode)
|
||||
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':
|
||||
@@ -179,17 +131,15 @@ def run():
|
||||
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)
|
||||
run_cmdv(node, cmdv, all, pipedesc)
|
||||
elif 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)
|
||||
@@ -197,21 +147,19 @@ def run():
|
||||
sys.stdout.write('{0}: {1}'.format(node, line))
|
||||
sys.stdout.flush()
|
||||
if all:
|
||||
rdy = poller.poll(10)
|
||||
sys.exit(exitcode | autoexitcode)
|
||||
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}
|
||||
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__':
|
||||
|
||||
+113
-119
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
__author__ = 'alin37'
|
||||
|
||||
import asyncio
|
||||
from getpass import getpass
|
||||
import optparse
|
||||
import os
|
||||
@@ -35,130 +34,125 @@ 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')
|
||||
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()
|
||||
|
||||
|
||||
#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
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
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
|
||||
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()
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
exitcode=client.updateattrib(session,arglist,nodetype, noderange, options, None)
|
||||
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,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)
|
||||
+106
-125
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# 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
|
||||
@@ -62,7 +62,7 @@ argparser.add_option('-e', '--extra', dest='extra',
|
||||
'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 +73,11 @@ 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')
|
||||
'Currently only uefi is supported')
|
||||
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')
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
cfgpaths = {
|
||||
@@ -99,12 +96,6 @@ cfgpaths = {
|
||||
'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'),
|
||||
}
|
||||
@@ -173,7 +164,7 @@ def parse_config_line(arguments, single=False):
|
||||
if '=' in param or param[-1] == ':' or forceset:
|
||||
if setmode is None:
|
||||
setmode = True
|
||||
if not setmode:
|
||||
if setmode != True:
|
||||
bailout('Cannot do set and query in same command: Query detected but "{0}" appears to be set'.format(param))
|
||||
if '=' in param:
|
||||
key, _, value = param.partition('=')
|
||||
@@ -185,7 +176,7 @@ def parse_config_line(arguments, single=False):
|
||||
else:
|
||||
if setmode is None:
|
||||
setmode = False
|
||||
if setmode:
|
||||
if setmode != False:
|
||||
bailout('Cannot do set and query in same command: Set mode detected but "{0}" appears to be a query'.format(param))
|
||||
if '.' not in param:
|
||||
if param == 'bmc':
|
||||
@@ -225,114 +216,104 @@ 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), single=True)
|
||||
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:
|
||||
session.stop_if_noderange_over(noderange, options.maxnodes)
|
||||
if 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)
|
||||
elif options.restoredefault.lower() in (
|
||||
'bmc', 'imm', 'xcc'):
|
||||
for fr in session.update(
|
||||
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
|
||||
{'clear': True}):
|
||||
rcode |= client.printerror(fr)
|
||||
sys.exit(rcode)
|
||||
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)
|
||||
sys.stderr.write(
|
||||
'Unrecognized component to restore defaults: {0}\n'.format(
|
||||
options.restoredefault))
|
||||
sys.exit(1)
|
||||
if setmode:
|
||||
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:
|
||||
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)
|
||||
sys.exit(rcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
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]
|
||||
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 |= client.print_attrib_path(path, session, list(queryparms[path]),
|
||||
NullOpt(), queryparms[path])
|
||||
if printsys == 'all' or printextbmc or printbmc or printallbmc:
|
||||
if printbmc or not printextbmc:
|
||||
rcode |= client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
|
||||
session, printbmc, options, attrprefix='bmc.')
|
||||
if options.extra:
|
||||
rcode |= client.print_attrib_path(
|
||||
'/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
|
||||
session, printextbmc, options)
|
||||
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)
|
||||
|
||||
+33
-1099
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# 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)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -49,19 +48,8 @@ def armonce(nr, cli):
|
||||
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': ''}
|
||||
def setpending(nr, profile, cli):
|
||||
args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': ''}
|
||||
if not profile.startswith('genesis-'):
|
||||
args['deployment.stagedprofile'] = ''
|
||||
args['deployment.profile'] = ''
|
||||
@@ -79,10 +67,8 @@ 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('-m', '--maxnodes', help='Specifiy a maximum nodes to be deployed')
|
||||
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)
|
||||
@@ -92,15 +78,9 @@ def main(args):
|
||||
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:
|
||||
if not args.profile and args.network:
|
||||
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()
|
||||
@@ -110,63 +90,40 @@ def main(args):
|
||||
if 'error' in rsp:
|
||||
sys.stderr.write(rsp['error'] + '\n')
|
||||
sys.exit(1)
|
||||
profilebynode = {}
|
||||
if not args.clear and args.network and not args.prepareonly:
|
||||
rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', 'network',
|
||||
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')
|
||||
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
|
||||
elif 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('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)
|
||||
sys.stderr.write('No deployment profiles available, try osdeploy fiimport or imgutil capture\n')
|
||||
sys.exit(1)
|
||||
armonce(args.noderange, c)
|
||||
setpending(args.noderange, args.profile, profilebynode, c)
|
||||
setpending(args.noderange, args.profile, c)
|
||||
else:
|
||||
databynode = {}
|
||||
for r in c.read('/noderange/{0}/attributes/current'.format(args.noderange)):
|
||||
@@ -175,7 +132,7 @@ def main(args):
|
||||
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'):
|
||||
if attr in ('deployment.pendingprofile', 'deployment.apiarmed', 'deployment.stagedprofile', 'deployment.profile', 'deployment.state', 'deployment.state_detail'):
|
||||
databynode[node][attr] = dbn[node][attr].get('value', '')
|
||||
for node in sortutil.natural_sort(databynode):
|
||||
profile = databynode[node].get('deployment.pendingprofile', '')
|
||||
@@ -189,8 +146,6 @@ def main(args):
|
||||
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', '')
|
||||
@@ -201,35 +156,18 @@ def main(args):
|
||||
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')
|
||||
if args.network and not args.prepareonly:
|
||||
return rc
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2017 Lenovo
|
||||
@@ -15,18 +15,19 @@
|
||||
# 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']
|
||||
@@ -49,11 +50,10 @@ columnmapping = {
|
||||
}
|
||||
#TODO: add chassis uuid
|
||||
|
||||
async def register_endpoint(options, session, addr):
|
||||
def register_endpoint(options, session, addr):
|
||||
neednewline = False
|
||||
current = 0
|
||||
total = 0
|
||||
async for rsp in session.update('/discovery/register', {'addresses': addr}):
|
||||
for rsp in session.update('/discovery/register', {'addresses': addr}):
|
||||
if 'count' in rsp:
|
||||
total = rsp['count']
|
||||
elif total > 1:
|
||||
@@ -69,28 +69,29 @@ async def register_endpoint(options, session, addr):
|
||||
if neednewline:
|
||||
print('')
|
||||
|
||||
async def subscribe_discovery(options, session, subscribe, targ):
|
||||
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):
|
||||
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)):
|
||||
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)):
|
||||
else:
|
||||
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
|
||||
@@ -122,7 +123,7 @@ 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')
|
||||
@@ -159,12 +160,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,10 +180,7 @@ 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
|
||||
@@ -193,7 +191,6 @@ async def import_csv(options, session):
|
||||
if field in unique_fields:
|
||||
unique_data[field] = set([])
|
||||
broken = False
|
||||
alldata=[]
|
||||
for record in records:
|
||||
currfields = list(fields)
|
||||
nodedatum = {}
|
||||
@@ -210,15 +207,9 @@ 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) and not broken:
|
||||
blocking_scan(session)
|
||||
if not search_record(nodedatum, options, session):
|
||||
sys.stderr.write(
|
||||
"Could not match the following data: " +
|
||||
repr(nodedatum) + '\n')
|
||||
@@ -226,13 +217,11 @@ async def import_csv(options, session):
|
||||
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)
|
||||
@@ -241,26 +230,9 @@ async def import_csv(options, session):
|
||||
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)
|
||||
@@ -269,10 +241,11 @@ async def assign_macs(maclist, nodename, assignlimit):
|
||||
print('Discovered ' + res['assigned'])
|
||||
else:
|
||||
print(repr(res))
|
||||
return exitcode
|
||||
if exitcode:
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
async def list_discovery(options, session):
|
||||
def list_discovery(options, session):
|
||||
orderby = None
|
||||
if options.fields:
|
||||
columns = []
|
||||
@@ -288,24 +261,23 @@ 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, node=None, checknode=True):
|
||||
path = '/discovery/'
|
||||
if node:
|
||||
path += 'by-node/{0}/'.format(node)
|
||||
@@ -324,22 +296,23 @@ 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):
|
||||
path += 'by-mac/'
|
||||
ret = []
|
||||
for x in session.read(path):
|
||||
if 'item' in x and 'href' in x['item']:
|
||||
yield x['item']['href']
|
||||
ret.append(x['item']['href'])
|
||||
return ret
|
||||
|
||||
async def assign_discovery(options, session, needid=True):
|
||||
def assign_discovery(options, session, needid=True):
|
||||
abort = False
|
||||
if options.importfile:
|
||||
return await import_csv(options, session)
|
||||
return import_csv(options, session)
|
||||
if not options.node:
|
||||
sys.stderr.write("Node (-n) must be specified for assignment\n")
|
||||
abort = True
|
||||
@@ -350,19 +323,16 @@ async def assign_discovery(options, session, needid=True):
|
||||
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, None if needid else options.node, False)
|
||||
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, None if needid else options.node, False)
|
||||
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']))
|
||||
@@ -374,14 +344,14 @@ async def assign_discovery(options, session, needid=True):
|
||||
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]')
|
||||
# -a for 'address' maybe?
|
||||
@@ -391,9 +361,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 +375,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')
|
||||
@@ -435,23 +402,23 @@ async def main():
|
||||
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)
|
||||
assign_discovery(options, session)
|
||||
if args[0] == 'reassign':
|
||||
await assign_discovery(options, session, False)
|
||||
assign_discovery(options, session, False)
|
||||
if args[0] == 'register':
|
||||
await register_endpoint(options, session, args[1])
|
||||
register_endpoint(options, session, args[1])
|
||||
if args[0] == 'subscribe':
|
||||
await subscribe_discovery(options, session, True, args[1])
|
||||
subscribe_discovery(options, session, True, args[1])
|
||||
if args[0] == 'unsubscribe':
|
||||
await subscribe_discovery(options, session, False, args[1])
|
||||
subscribe_discovery(options, session, False, args[1])
|
||||
if args[0] == 'rescan':
|
||||
await blocking_scan(session, aggressive=options.aggressive)
|
||||
blocking_scan(session)
|
||||
print("Rescan complete")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -53,13 +53,7 @@ argparser.add_option('-t', '--timeframe', type='string',
|
||||
'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')
|
||||
)
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
@@ -78,30 +72,14 @@ if len(args) == 2:
|
||||
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'] + ':')
|
||||
@@ -144,7 +122,6 @@ if options.timeframe:
|
||||
sys.exit(1)
|
||||
timeframe = dt.now() - tdelta
|
||||
|
||||
sources_seen = {}
|
||||
event_dict = {}
|
||||
nodes = []
|
||||
for res in session.read('/noderange/{0}/nodes/'.format(args[0])):
|
||||
@@ -165,17 +142,6 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
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:
|
||||
@@ -189,34 +155,7 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
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:
|
||||
if options.lines:
|
||||
for node in nodes:
|
||||
evtdata_list = event_dict[node]
|
||||
if len(evtdata_list) != 0:
|
||||
@@ -230,5 +169,3 @@ if options.lines and not listmode:
|
||||
print('{0}: {1}'.format(node, format_event(evt)))
|
||||
else:
|
||||
print('{0}: {1}'.format(node, format_event(evt)))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -56,35 +56,23 @@ 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 = []
|
||||
@@ -102,7 +90,7 @@ 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))
|
||||
detail = res.get('detail', repr(res)),
|
||||
if status == 'error':
|
||||
text = 'error!'
|
||||
else:
|
||||
@@ -120,37 +108,15 @@ 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
|
||||
of = open(filename, 'rb')
|
||||
try:
|
||||
session.add_file(filename, 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(
|
||||
@@ -183,13 +149,9 @@ def show_firmware(session):
|
||||
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)):
|
||||
'/noderange/{0}/inventory/firmware/all/{1}'.format(
|
||||
noderange, component)):
|
||||
nodes_matched = True
|
||||
exitcode |= client.printerror(res)
|
||||
if 'databynode' not in res:
|
||||
@@ -204,36 +166,12 @@ def show_firmware(session):
|
||||
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)))
|
||||
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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -84,13 +84,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:
|
||||
|
||||
@@ -38,8 +38,6 @@ if sys.version_info[0] < 3:
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
filters = []
|
||||
wanted = []
|
||||
matched = False
|
||||
|
||||
|
||||
def pretty(text):
|
||||
@@ -51,9 +49,7 @@ 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']:
|
||||
memdescfmt += '4-{1} '
|
||||
@@ -62,21 +58,16 @@ def print_mem_info(node, prefix, meminfo):
|
||||
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):
|
||||
memdescfmt += 'ECC '
|
||||
capacity = meminfo['capacity_mb'] / 1024
|
||||
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'
|
||||
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']))
|
||||
@@ -123,10 +114,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':
|
||||
@@ -207,7 +196,6 @@ try:
|
||||
continue
|
||||
if info[datum] is None:
|
||||
continue
|
||||
matched = True
|
||||
if options.json:
|
||||
if node not in databynode:
|
||||
databynode[node] = {}
|
||||
@@ -217,11 +205,6 @@ try:
|
||||
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))
|
||||
if options.json:
|
||||
print(json.dumps(databynode, sort_keys=True, indent=4,
|
||||
separators=(',', ': ')))
|
||||
|
||||
@@ -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/python2
|
||||
# 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)
|
||||
@@ -64,30 +65,16 @@ 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
|
||||
of = open(filename, 'rb')
|
||||
try:
|
||||
session.add_file(filename, 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]:
|
||||
@@ -119,7 +106,6 @@ def show_licenses(session):
|
||||
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',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/libexec/platform-python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
@@ -21,7 +21,6 @@ import optparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
|
||||
|
||||
@@ -34,14 +33,14 @@ 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')
|
||||
help='Show information about how attributes inherited')
|
||||
argparser.add_option('-d', '--delim', metavar="STRING", default = "\n",
|
||||
help='Delimiter separating the values')
|
||||
(options, args) = argparser.parse_args()
|
||||
@@ -60,18 +59,18 @@ 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(options.delim.join(list))
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2018 Lenovo
|
||||
@@ -101,37 +101,22 @@ 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
|
||||
of = open(filename, 'rb')
|
||||
try:
|
||||
session.add_file(filename, of.fileno(), 'rb')
|
||||
except Exception:
|
||||
pass
|
||||
nodeurls = {}
|
||||
for res in session.create(resource, upargs):
|
||||
if 'created' not in res:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -42,8 +42,6 @@ def run():
|
||||
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
|
||||
@@ -58,7 +56,6 @@ def run():
|
||||
|
||||
currprocs = 0
|
||||
all = set([])
|
||||
poller = select.epoll()
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
@@ -86,29 +83,19 @@ def run():
|
||||
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)
|
||||
run_cmdv(pingnode, cmdv, all, pipedesc)
|
||||
else:
|
||||
if options.origname:
|
||||
pendingexecs.append((node, cmdv))
|
||||
else:
|
||||
pendingexecs.append((pingnode, cmdv))
|
||||
pendingexecs.append((pingnode, 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 not data:
|
||||
pop = desc['popen']
|
||||
@@ -116,7 +103,6 @@ def run():
|
||||
if ret is not None:
|
||||
exitcode = exitcode | ret
|
||||
all.discard(r)
|
||||
poller.unregister(r)
|
||||
r.close()
|
||||
if desc['type'] == 'stdout':
|
||||
if ret:
|
||||
@@ -125,8 +111,7 @@ def run():
|
||||
print('{0}: ping'.format(node))
|
||||
if 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)
|
||||
@@ -136,21 +121,19 @@ def run():
|
||||
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, 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}
|
||||
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__':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# 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,26 @@ 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())
|
||||
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)
|
||||
|
||||
@@ -35,6 +35,7 @@ if path.startswith('/opt'):
|
||||
|
||||
import confluent.client as client
|
||||
import confluent.screensqueeze as sq
|
||||
import confluent.sortutil as sortutil
|
||||
|
||||
|
||||
def run():
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -67,7 +67,6 @@ def run():
|
||||
|
||||
currprocs = 0
|
||||
all = set([])
|
||||
poller = select.epoll()
|
||||
pipedesc = {}
|
||||
pendingexecs = deque()
|
||||
exitcode = 0
|
||||
@@ -85,23 +84,19 @@ def run():
|
||||
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':
|
||||
@@ -121,12 +116,10 @@ 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)
|
||||
@@ -136,11 +129,11 @@ def run():
|
||||
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):
|
||||
try:
|
||||
nopen = subprocess.Popen(
|
||||
cmdv, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@@ -149,14 +142,13 @@ def run_cmdv(node, cmdv, all, poller, pipedesc):
|
||||
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}
|
||||
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()
|
||||
|
||||
@@ -67,7 +67,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 +107,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,14 +120,10 @@ 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:
|
||||
@@ -138,20 +134,17 @@ def sensorpass(showout=True, appendtime=False):
|
||||
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']))
|
||||
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':
|
||||
datadescription = [sensedata['health']]
|
||||
@@ -265,6 +258,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)
|
||||
|
||||
@@ -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|usb|floppy]')
|
||||
argparser.add_option('-b', '--bios', dest='biosmode',
|
||||
action='store_true', default=False,
|
||||
help='Request BIOS style boot (rather than UEFI)')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/python2
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2016-2017 Lenovo
|
||||
@@ -53,8 +53,6 @@ def run():
|
||||
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',
|
||||
@@ -111,7 +109,6 @@ def run():
|
||||
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):
|
||||
@@ -121,28 +118,22 @@ def run():
|
||||
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)
|
||||
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':
|
||||
@@ -162,12 +153,10 @@ 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)
|
||||
@@ -178,21 +167,19 @@ def run():
|
||||
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}
|
||||
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__':
|
||||
|
||||
@@ -244,8 +244,6 @@ def main():
|
||||
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__':
|
||||
|
||||
@@ -76,11 +76,6 @@ def download_servicedata(noderange, media, options):
|
||||
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(
|
||||
@@ -153,9 +148,5 @@ def main():
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -19,7 +19,6 @@ import argparse
|
||||
import base64
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import numpy as np
|
||||
import sys
|
||||
|
||||
@@ -73,8 +72,6 @@ def plot(gui, output, plotdata, bins, fmt):
|
||||
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)
|
||||
@@ -111,7 +108,7 @@ 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('-f', default='sixel', 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')
|
||||
|
||||
@@ -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)
|
||||
@@ -57,7 +57,7 @@ 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')
|
||||
return instr.decode('utf-8')
|
||||
elif not isinstance(instr, bytes) and not isinstance(instr, str):
|
||||
return instr.encode('utf-8')
|
||||
return instr
|
||||
@@ -280,7 +280,6 @@ class Command(object):
|
||||
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:
|
||||
@@ -392,13 +391,8 @@ 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()
|
||||
@@ -470,24 +464,10 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, 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])
|
||||
return k[1]['sortid']
|
||||
return 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, attrprefix=None):
|
||||
exitcode = 0
|
||||
seenattributes = NestedDict()
|
||||
allnodes = set([])
|
||||
@@ -515,26 +495,12 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
|
||||
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:
|
||||
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).strip()
|
||||
else:
|
||||
attrout = '{0}: {1}:'.format(node, printattr)
|
||||
elif 'isset' in currattr:
|
||||
@@ -582,19 +548,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:
|
||||
@@ -633,7 +595,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None, attrpref
|
||||
else:
|
||||
printmissing.add(attr)
|
||||
for missing in printmissing:
|
||||
sys.stderr.write('Error: {0} not a valid attribute\n'.format(missing))
|
||||
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
|
||||
return exitcode
|
||||
|
||||
|
||||
@@ -706,40 +668,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 +702,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)
|
||||
|
||||
@@ -175,10 +175,6 @@ class LogReplay(object):
|
||||
|
||||
|
||||
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())
|
||||
|
||||
@@ -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_screengeom():
|
||||
return struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
|
||||
b'....'))
|
||||
class ScreenPrinter(object):
|
||||
|
||||
def __init__(self, noderange, client, textlen=4):
|
||||
@@ -103,7 +58,7 @@ class ScreenPrinter(object):
|
||||
|
||||
def drawscreen(self, node=None):
|
||||
if self.squeeze:
|
||||
currheight, currwidth, _, _ = get_screengeom()
|
||||
currheight, currwidth = get_screengeom()
|
||||
currheight -= 2
|
||||
if currheight < 1:
|
||||
currheight = 1
|
||||
@@ -165,7 +120,6 @@ if __name__ == '__main__':
|
||||
c = client.Command()
|
||||
p = ScreenPrinter('d1-d12', c)
|
||||
p.set_output('d3', 'Upload: 67%')
|
||||
p.set_output('d7', 'Upload: 67%')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -19,8 +19,12 @@ import array
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import confluent.tlv as tlv
|
||||
import socket
|
||||
import select
|
||||
try:
|
||||
import eventlet.green.socket as socket
|
||||
import eventlet.green.select as select
|
||||
except ImportError:
|
||||
import socket
|
||||
import select
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -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,17 +1,13 @@
|
||||
%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: Apache2
|
||||
Group: Development/Libraries
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
|
||||
Prefix: %{_prefix}
|
||||
@@ -19,20 +15,13 @@ BuildArch: noarch
|
||||
Vendor: Lenovo
|
||||
Url: http://github.com/lenovo/confluent
|
||||
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"
|
||||
@@ -48,12 +37,6 @@ python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUI
|
||||
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
|
||||
|
||||
%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
|
||||
|
||||
@@ -19,7 +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'
|
||||
@@ -53,8 +52,6 @@ _confluent_get_args()
|
||||
CMPARGS+=("")
|
||||
fi
|
||||
GENNED=""
|
||||
# Whitespace separates candidate groups, while commas group synonyms.
|
||||
# shellcheck disable=SC2068
|
||||
for CAND in ${COMP_CANDIDATES[@]}; do
|
||||
candarray=(${CAND//,/ })
|
||||
matched=0
|
||||
@@ -156,22 +153,14 @@ _confluent_osimage_completion()
|
||||
{
|
||||
_confluent_get_args
|
||||
if [ $NUMARGS == 2 ]; then
|
||||
COMPREPLY=($(compgen -W "initialize import importcheck updateboot rebase fetch" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
COMPREPLY=($(compgen -W "initialize import updateboot rebase" -- ${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
|
||||
COMPREPLY=($(compgen -W "-h -u -s -t -i" -- ${COMP_WORDS[COMP_CWORD]}))
|
||||
elif [ ${CMPARGS[1]} == 'import' ]; 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
|
||||
@@ -304,30 +293,6 @@ _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
|
||||
@@ -343,22 +308,6 @@ _confluent_nn_completion()
|
||||
|
||||
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
|
||||
}
|
||||
|
||||
_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)
|
||||
@@ -413,7 +362,7 @@ complete -F _confluent_nodeattrib_completion nodeattrib
|
||||
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_nn_completion nodeconsole
|
||||
complete -F _confluent_define_completion nodedefine
|
||||
complete -F _confluent_define_completion nodegroupdefine
|
||||
complete -F _confluent_nr_completion nodeeventlog
|
||||
@@ -426,7 +375,6 @@ 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_nodemedia_completion nodemedia
|
||||
complete -F _confluent_nodepower_completion nodepower
|
||||
|
||||
@@ -42,7 +42,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)
|
||||
@@ -13,7 +13,7 @@ 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.
|
||||
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.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
|
||||
@@ -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,26 +3,16 @@ 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`:
|
||||
@@ -34,13 +24,10 @@ stored one-way hashes.
|
||||
|
||||
* `-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 `********`.
|
||||
The keys.json file will include the encryption keys without any protection.
|
||||
|
||||
* `-s`, `--skipkeys`:
|
||||
This specifies to dump the encrypted data without
|
||||
@@ -48,46 +35,8 @@ stored one-way hashes.
|
||||
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
|
||||
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`:
|
||||
|
||||
* `-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`
|
||||
|
||||
@@ -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
|
||||
@@ -24,13 +24,6 @@ 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`.
|
||||
|
||||
@@ -38,15 +31,11 @@ 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
|
||||
See nodegroupattrib(8) command on 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.
|
||||
|
||||
@@ -72,8 +61,8 @@ to a blank value will allow masking a group defined attribute with an empty valu
|
||||
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
|
||||
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 simmilar 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.
|
||||
|
||||
@@ -122,29 +111,6 @@ to a blank value will allow masking a group defined attribute with an empty valu
|
||||
`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`
|
||||
|
||||
@@ -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`
|
||||
@@ -1,50 +0,0 @@
|
||||
nodecertutil(8) -- Manage BMC CA certificates and sign BMC certificates
|
||||
=======================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodecertutil <noderange> <command>` [<args>]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodecertutil` manages the certificate authority (CA) certificates trusted by
|
||||
the baseboard management controllers (BMCs) of the nodes matched by
|
||||
`<noderange>`, and can sign a BMC certificate. One of the subcommands below
|
||||
must be supplied.
|
||||
|
||||
## COMMANDS
|
||||
|
||||
* `installbmccacert <filename>`:
|
||||
Install the CA certificate contained in `<filename>` onto the BMCs.
|
||||
|
||||
* `removebmccacert <id>`:
|
||||
Remove the CA certificate identified by `<id>` from the BMCs. Use
|
||||
`listbmccacerts` to discover the identifiers.
|
||||
|
||||
* `listbmccacerts`:
|
||||
List the CA certificates currently installed on the BMCs.
|
||||
|
||||
* `signbmccert` [`--days <days>`] [`--added-names <names>`]:
|
||||
Sign a BMC certificate. `--days` is required and sets the number of days
|
||||
the certificate is valid for; `--added-names` adds additional subject
|
||||
alternative names to the certificate.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* List the CA certificates installed on a node's BMC:
|
||||
`# nodecertutil n1 listbmccacerts`
|
||||
|
||||
* Install a CA certificate on a range of nodes:
|
||||
`# nodecertutil n1-n4 installbmccacert /etc/confluent/myca.pem`
|
||||
|
||||
* Sign a BMC certificate valid for one year:
|
||||
`# nodecertutil n1 signbmccert --days 365`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeconfig(8), nodeattrib(8)
|
||||
@@ -49,11 +49,8 @@ actually be in effect until a reboot.
|
||||
|
||||
* `-r COMPONENT`, `--restoredefault=COMPONENT`:
|
||||
Request that the specified component of the targeted nodes will have its
|
||||
configuration reset to default. The component may be "uefi" or "bmc".
|
||||
|
||||
* `-p, --pending`:
|
||||
When possible, indicate only the settings that are awaiting a reboot to take effect. Note that not all nodes will differentiate between
|
||||
pending and current settings, and for such platforms this will come up empty.
|
||||
configuration reset to default. Currently the only component implemented
|
||||
is uefi.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to configure, prompting if over
|
||||
@@ -72,7 +69,7 @@ actually be in effect until a reboot.
|
||||
`s4: bmc.ipv4_method: DHCP`
|
||||
`s4: bmc.ipv4_gateway: 172.30.0.6`
|
||||
|
||||
* Changing nodes `s3` and `s4` to have the IP addresses 10.1.2.3 and 10.1.2.4 with a 16 bit subnet mask:
|
||||
* Changing nodes `s3` and `s4` to have the ip addressess 10.1.2.3 and 10.1.2.4 with a 16 bit subnet mask:
|
||||
`# nodeconfig s3,s4 bmc.ipv4_address=10.1.2.{n1}/16`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
@@ -21,15 +21,9 @@ console process which will result in the console window closing.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-i N`, `--interval`:
|
||||
For screenshot mode, fetch new screenshots and overwrite old screenshots every N seconds.
|
||||
For example, `nodeconsole r3u[21:24] -tsi 3` will tile screenshots of r3u21 through r3u24 and
|
||||
refresh them every 3 seconds.
|
||||
|
||||
* `-t`, `--tile`:
|
||||
For text consoles, use tmux to arrange consoles of the given noderange into a tiled layout on
|
||||
the terminal screen. If using 'screenshot' mode, divide the terminal and display the images
|
||||
in a grid.
|
||||
Use tmux to arrange consoles of the given noderange into a tiled layout on
|
||||
the terminal screen
|
||||
|
||||
* `-l`, `--log`:
|
||||
Perform a log reply on the current, local log in /var/log/confluent/consoles.
|
||||
@@ -37,17 +31,9 @@ console process which will result in the console window closing.
|
||||
manager at this time.
|
||||
|
||||
* `-T`, `--Timestamp`:
|
||||
Dump the log with Timestamps on the current, local log in /var/log/confluent/consoles.
|
||||
Dump the log with Timpstamps on the current, local log in /var/log/confluent/consoles.
|
||||
If in collective mode, this only makes sense to use on the current collective
|
||||
manager at this time.
|
||||
|
||||
* `-s`, `--screenshot`:
|
||||
Attempt to grab screenshot(s) and render using a terminal
|
||||
image protocol. The image protocol defaults to kitty, and
|
||||
can be selected by CONFLUENT_IMAGE_PROTOCOL environment variable.
|
||||
Supported protocols are kitty, iterm, and, sixel (sixel only
|
||||
if PySixel is installed). This only presents screenshots, there
|
||||
is no input supported to graphical consoles from a terminal.
|
||||
|
||||
* `-w`, `--windowed`:
|
||||
Open terminal windows for each node. The
|
||||
@@ -65,9 +51,9 @@ console process which will result in the console window closing.
|
||||
environment, to open a set of consoles for a range of
|
||||
nodes in separate Windows Terminal windows, with the
|
||||
title set for each node, set **NODECONSOLE_WINDOWED_COMMAND**
|
||||
to `wt.exe wsl.exe -d AlmaLinux-8 --shell-type login`. If the
|
||||
to `wt.exe wsl.exe -d AlmaLinux-8 --shell-type login. If the
|
||||
NODECONSOLE_WINDOWED_COMMAND environment variable isn't set,
|
||||
xterm will be used by default.
|
||||
xterm will be used bydefault.
|
||||
|
||||
## ESCAPE SEQUENCE COMMANDS
|
||||
|
||||
@@ -102,7 +88,7 @@ keystroke will be interpreted as a command. The following commands are availabl
|
||||
Request immediate boot to network
|
||||
* `r`:
|
||||
[send Resize]
|
||||
This queries the current terminal and sends stty commands to advertise the user terminal
|
||||
This queries the current terminal and sends stty commands to advertise the user termineal
|
||||
size to the remote console
|
||||
* `?`:
|
||||
Get a list of supported commands
|
||||
@@ -112,10 +98,10 @@ keystroke will be interpreted as a command. The following commands are availabl
|
||||
|
||||
## PASSTHROUGH OPTIONS
|
||||
|
||||
While opening a windowed console with xterm or any other console of choice. The
|
||||
nodeconsole command gives capability to specify passthrough options targeted at
|
||||
the console. All options after the -- will be passed to the console program. For
|
||||
example, opening a windowed console using xterm with a black background.
|
||||
`nodeconsole -w n1 -- -bg black`
|
||||
While opening a windowed console with xterm or any other console of choice. The
|
||||
nodeconsole command gives capality to specify passthrough options targeted at
|
||||
the console. All options after the -- will be parsed the console program. For
|
||||
example, opening a windowed console using xterm with a black background.
|
||||
`nodeconconsole -w n1 -- -bg black`
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ nodedefine(8) -- Define new confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodedefine <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodedefine <noderange> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -26,4 +26,4 @@ that `nodeattrib(8)` will error if a node does not exist.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
noderange(5), nodeattribexpressions(5)
|
||||
noderange(5), nodeattribexpressions(8)
|
||||
|
||||
@@ -27,14 +27,14 @@ deployment status.
|
||||
Prepare the network services for deployment, but do not interact with BMCs. This is intended for scenarios where
|
||||
the boot device control and server restart will be handled outside of confluent.
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specify a maximum number of nodes to be deployed.
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
Specifiy a maximum nodes to be deployed.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Begin the installation of a profile of CentOS 8.2:
|
||||
* Begin the instalalation of a profile of CentOS 8.2:
|
||||
`# nodedeploy d4 -n centos-8.2-x86_64-default`
|
||||
`d4: network`
|
||||
`d4: reset`
|
||||
|
||||
@@ -35,8 +35,8 @@ devices. Generally every effort is made to passively detect devices as they
|
||||
become available (as they boot or are plugged in), however sometimes an active
|
||||
scan is the best approach to catch something that appears to be missing.
|
||||
|
||||
**nodediscover clear** requests the server to forget about a selection of
|
||||
detected devices. It takes the same arguments as **nodediscover list**.
|
||||
**nodedsicover clear** requests the server forget about a selection of
|
||||
detected device. It takes the same arguments as **nodediscover list**.
|
||||
|
||||
**nodediscover subscribe** and **unsubscribe** instructs confluent to subscribe to or
|
||||
unsubscribe from the designated switch running affluent with system discovery support.
|
||||
@@ -75,9 +75,6 @@ the nodes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-a`, `--aggressive`:
|
||||
Perform more aggressive scanning and fingerprinting. This will probe as many addresses as it can find with ssh and https connections. By default more targeted measures are used
|
||||
that will only tend to probe things that are easily detectable as devices implementing explicit mechanisms for detection and identification.
|
||||
* `-m MODEL`, `--model=MODEL`:
|
||||
Operate with nodes matching the specified model number
|
||||
* `-s SERIAL`, `--serial=SERIAL`:
|
||||
|
||||
@@ -3,28 +3,13 @@ nodeeventlog(8) -- Pull eventlog from confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeeventlog [options] [-s <sources>] <noderange> [clear]`
|
||||
`nodeeventlog [options] <noderange> [clear]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeeventlog` pulls and optionally clears the event log from the requested
|
||||
noderange.
|
||||
|
||||
A platform may keep its events in more than one log, and some of those are
|
||||
noisier than others. Reading gathers every log the platform describes as an
|
||||
event log, wherever it keeps them, and `-s` narrows the output to the ones
|
||||
asked for. `-s list` names the logs a node offers rather than showing entries.
|
||||
|
||||
A source is named by the entries that come from it, so a log that holds no
|
||||
entries is not among them, and a node whose logs are all empty names none. An
|
||||
ipmi managed node keeps its events in the one log the spec gives it, which is
|
||||
reported as `SEL`.
|
||||
|
||||
Clearing is deliberately narrower than reading: it only touches the logs the
|
||||
platform's own manager publishes, so a log that only a read reaches is never
|
||||
destroyed by one. A selection cannot be cleared, so `-s` and `clear` are
|
||||
refused together.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
@@ -35,31 +20,15 @@ refused together.
|
||||
return the last <n> entries for each node in the eventlog.
|
||||
|
||||
* `-t TIMEFRAME`, `--timeframe=TIMEFRAME`:
|
||||
return entries within a specified timeframe for each node's event log.
|
||||
This will return entries from the last <n> hours or days. 1h would be
|
||||
entries from within the last one hour.
|
||||
return entries within a specified timeframe for each node's event log.
|
||||
This will return entries from the last <n> hours or days. 1h would be
|
||||
entries from with the last one hour.
|
||||
|
||||
|
||||
* `-s SOURCES`, `--source=SOURCES`:
|
||||
only show entries from the named logs, comma delimited and matched without
|
||||
regard to case. `-s list` names the logs the node offers instead of showing
|
||||
entries. A platform that named a log "list" would be shadowed by that.
|
||||
Asking for a log the node named nothing from is reported on stderr and
|
||||
exits non-zero, rather than looking like an empty log.
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
* Ask which logs a node keeps:
|
||||
`# nodeeventlog n2 -s list`
|
||||
`n2: AuditLog,EventLog,Logs,SEL`
|
||||
|
||||
* Pull only the sel and the chassis log, leaving the audit noise out:
|
||||
`# nodeeventlog n2 -s SEL,Logs`
|
||||
`n2: 07/23/2026 13:03:12 SEL: Processor - Thermal Trip`
|
||||
`n2: 08/05/2026 04:30:26 Logs: PowerUnit - Power Off`
|
||||
|
||||
* Pull the event log from n2 and n3:
|
||||
`# nodeeventlog n2,n3`
|
||||
`n2: 05/03/2017 11:44:25 Event Log Disabled - SEL Fullness - Log clear`
|
||||
|
||||
@@ -3,7 +3,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodefirmware <noderange> [list][updatestatus][updatetypes][update [--backup] [--parameterfile <file>] <file>]|[<components>]`
|
||||
`nodefirmware <noderange> [list][update [--backup <file>]]|[<components>]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -17,13 +17,6 @@ not be relevant to redfish. Additionally, the Lenovo XCC makes certain
|
||||
information available over IPMI that is not otherwise available (for example
|
||||
the FPGA version where applicable).
|
||||
|
||||
The updatestatus argument will describe the state of firmware updates on the
|
||||
nodes.
|
||||
|
||||
The updatetypes argument will name the kinds of firmware image the platform has
|
||||
to be told about, for use in a parameter file. Platforms that read the kind of
|
||||
firmware from the image itself have none to name and say so.
|
||||
|
||||
In the update form, it accepts a single file and attempts to update it using
|
||||
the out of band facilities. Firmware updates can end in one of three states:
|
||||
|
||||
@@ -39,43 +32,12 @@ the out of band facilities. Firmware updates can end in one of three states:
|
||||
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
|
||||
When updating, prompt if more than the specified number of servers will
|
||||
be affected
|
||||
|
||||
* `-p PARAMETERFILE`, `--parameterfile=PARAMETERFILE`:
|
||||
For updating, a parameter file to provide along with the update payload. See
|
||||
PARAMETER FILE below
|
||||
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## PARAMETER FILE
|
||||
|
||||
The parameter file given with `-p` holds JSON that is sent alongside the image.
|
||||
Its keys are the parts a Redfish multipart update accepts, so `UpdateParameters`
|
||||
for the standard fields and `OemParameters` for whatever a vendor asks for on top
|
||||
of them. Without one, an update names no targets, which means "whatever this
|
||||
image is for", and that is what most platforms want.
|
||||
|
||||
Some platforms will not take an image without being told what kind of firmware it
|
||||
holds, and refuse the update rather than guess. On an AMI MegaRAC that is an
|
||||
`ImageType` under `OemParameters`:
|
||||
|
||||
{"OemParameters": {"ImageType": "BIOS"}}
|
||||
|
||||
`nodefirmware <noderange> updatetypes` names the accepted types without
|
||||
attempting an update. The value is also checked against the same list before
|
||||
anything is uploaded, so a name the platform does not accept is refused rather
|
||||
than sent.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Ask what kinds of firmware image a node has to be told about:
|
||||
`# nodefirmware n1 updatetypes`
|
||||
`n1: BMC,BIOS,MB_CPLD,SCM_CPLD,BPB_CPLD,HPM_BMC,HPM_BIOS,HPM_SCP,HPM_BIOS2`
|
||||
|
||||
* Update the BIOS on a platform that has to be told:
|
||||
`# echo '{"OemParameters": {"ImageType": "BIOS"}}' > /tmp/bios.json`
|
||||
`# nodefirmware n1 update -p /tmp/bios.json /tmp/image.bin`
|
||||
|
||||
* Pull firmware from a node:
|
||||
`# nodefirmware r1`
|
||||
`r1: IMM: 3.70 (TCOO26H 2016-11-29T05:09:51)`
|
||||
|
||||
@@ -11,7 +11,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegroupattrib` queries the confluent server to get information about nodes.
|
||||
`nodegroupattrip` queries the confluent server to get information about nodes.
|
||||
In the simplest form, it simply takes the given group and lists the attributes of that group.
|
||||
|
||||
Contrasted with nodeattrib(8), settings managed by nodegroupattrib will be added
|
||||
@@ -23,10 +23,6 @@ node after using the nodeattrib(8) command will not have attributes change autom
|
||||
It's easiest to see by using the `nodeattrib <noderange> -b` to understand how
|
||||
the attributes are set on the node versus a group to which a node belongs.
|
||||
|
||||
The values of `secret.*` and `crypted.*` attributes are never shown here; they
|
||||
can only be retrieved on the confluent server with
|
||||
`confluentdbutil -u showattrib`.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-b`, `--blame`:
|
||||
|
||||
@@ -3,7 +3,7 @@ nodegroupdefine(8) -- Define new confluent node group
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegroupdefine <groupname> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
`nodegroupdefine <groupname> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
@@ -21,4 +21,4 @@ that `nodegroupattrib(8)` will error if a node group does not exist.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodeattribexpressions(5), nodegroupattrib(8), nodegroupremove(8)
|
||||
nodeattribexpressions(8), nodegroupattrib(8), nodegroupremove(8)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
nodegrouprename(8) -- Rename a node group in the confluent management service
|
||||
=============================================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodegrouprename <group> <newname>`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodegrouprename` changes the name of an existing node group from `<group>` to
|
||||
`<newname>`. Group membership and the attributes defined on the group are
|
||||
preserved under the new name.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
* Rename a group:
|
||||
`# nodegrouprename compute computenodes`
|
||||
`compute: computenodes`
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
nodegroupdefine(8), nodegroupremove(8), nodegroupattrib(8), nodegrouplist(8)
|
||||
@@ -3,11 +3,11 @@ nodeidentify(8) -- Control the identify LED of confluent nodes
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`nodeidentify <noderange> [on|off|blink]`
|
||||
`nodidentify <noderange> [on|off|blink]`
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
`nodeidentify` allows you to turn on or off the location LED of confluent nodes,
|
||||
`nodeidentify` allows you to turn on or off the location LED of conflueunt nodes,
|
||||
making it easier to determine the physical location of the nodes. The following
|
||||
options are supported:
|
||||
|
||||
@@ -24,7 +24,7 @@ options are supported:
|
||||
`n3: on`
|
||||
`n4: on`
|
||||
|
||||
* Turn off the identify LED on nodes n1 through n4:
|
||||
* Turn off the identify LED on nodes n1 thorugh n4:
|
||||
`# nodeidentify n1-n4 off`
|
||||
`n1: off`
|
||||
`n2: off`
|
||||
|
||||
@@ -9,7 +9,7 @@ nodeinventory(8) -- Get hardware inventory of confluent node
|
||||
|
||||
`nodeinventory` pulls information about hardware of a node. This includes
|
||||
information such as adapters, serial numbers, processors, and memory modules,
|
||||
as supported by the platform's hardware management implementation. It accepts
|
||||
as supported by the platforms hardware management implementation. It accepts
|
||||
arguments such as serial or model or others as listed above to filter
|
||||
output to specific data.
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
nodel2traceroute(8) -- Returns the layer 2 route through an Ethernet network managed by confluent given 2 end points.
|
||||
==============================
|
||||
## SYNOPSIS
|
||||
`nodel2traceroute [options] <start_node> <end_noderange>`
|
||||
|
||||
## DESCRIPTION
|
||||
**nodel2traceroute** is a command that returns the layer 2 route for the configured interfaces in nodeattrib.
|
||||
It can also be used with the -i and -e options to check against specific interfaces on the endpoints. If the
|
||||
--interface or --eface option are not used then the command will check for routes against all the defined
|
||||
interfaces in nodeattrib (net.*.switch) for the nodes.
|
||||
|
||||
|
||||
|
||||
## PREREQUISITES
|
||||
**nodel2traceroute** the net.<interface>.switch attributes have to be set on the end points if endpoint is not a switch
|
||||
|
||||
|
||||
## OPTIONS
|
||||
* ` -e` EFACE, --eface=INTERFACE
|
||||
interface to check against for the second end point or end points if checking against multiple nodes
|
||||
* ` -i` INTERFACE, --interface=INTERFACE
|
||||
interface to check against for the first end point
|
||||
* ` -c` CUMULUS, --cumulus=CUMULUS
|
||||
return layer 2 route through cumulus switches only
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
* Checking route between two nodes:
|
||||
`# nodel2traceroute n244 n1851`
|
||||
`n244 to n1851: ['switch114']`
|
||||
|
||||
* Checking route from one node to multiple nodes:
|
||||
`# nodel2traceroute n244 n1833,n1851`
|
||||
`n244 to n1833: ['switch114', 'switch7', 'switch32', 'switch253', 'switch85', 'switch72', 'switch21', 'switch2', 'switch96', 'switch103', 'switch115']
|
||||
n244 to n1851: ['switch114']`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ nodelicense(8) -- Manage license keys on BMC
|
||||
## DESCRIPTION
|
||||
|
||||
`nodelicense` manages license keys on supported BMCs. Without an argument, the command
|
||||
lists currently installed licenses. Using `delete` will remove the specified license name
|
||||
lists currently installed license. Using `delete` will remove the specified license name
|
||||
from the BMC. The `save` subcommand will take the passed directory (which may be in the form
|
||||
of /path/to/{node}/ to have the node name substituted for each node) and back up installed licenses
|
||||
to that directory. The `install` command will take the specified filename and install. The filename
|
||||
|
||||
@@ -15,7 +15,7 @@ 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. There is more
|
||||
information on node attributes in the nodeattrib(8) man page.
|
||||
information on node attributes in nodeattributes(5) man page.
|
||||
|
||||
Attributes may be specified by wildcard, for example `net.*switch` will report
|
||||
all attributes that begin with `net.` and end with `switch`.
|
||||
@@ -25,7 +25,7 @@ all attributes that begin with `net.` and end with `switch`.
|
||||
* `-b`, `--blame`:
|
||||
Annotate inherited and expression based attributes to show their base value.
|
||||
* `-d`, `--delim`:
|
||||
Choose a delimiter to separate the values. Default - ENTER.
|
||||
Choose a delimiter to separat the values. Default - ENTER.
|
||||
## EXAMPLES
|
||||
* Listing matching nodes of a simple noderange:
|
||||
`# nodelist n1-n4`
|
||||
@@ -40,7 +40,7 @@ all attributes that begin with `net.` and end with `switch`.
|
||||
`n2: hardwaremanagement.manager: 172.30.3.2`
|
||||
|
||||
* Getting a group of attributes while determining what group defines them:
|
||||
`# nodelist n1,n2 hardwaremanagement --blame`
|
||||
`# nodelist n1,n2 hardwaremanegement --blame`
|
||||
`n1: hardwaremanagement.manager: 172.30.3.1`
|
||||
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
|
||||
`n1: hardwaremanagement.switch: r8e1`
|
||||
|
||||
@@ -32,13 +32,6 @@ BMCs map a virtual USB device to that url. Content is loaded on demand, and
|
||||
as such that URL is referenced potentially once for every IO operation that
|
||||
the host platform attempts.
|
||||
|
||||
## NOTES
|
||||
|
||||
When doing an attach of an https:// url, you may hit an error if you have not enrolled your certificate authority.
|
||||
In a general confluent environment, you can usually address it by:
|
||||
`# for cert in /var/lib/confluent/public/site/tls/*.pem; do nodecertutil s1-s4 installbmccacert $cert; done`
|
||||
|
||||
|
||||
## OPTIONS
|
||||
|
||||
* `-h`, `--help`:
|
||||
|
||||
@@ -14,8 +14,8 @@ It can also be used with the `-s` flag to change the ping location to something
|
||||
* `-h`, `--help`:
|
||||
Show help message and exit
|
||||
* `-s` SUBSTITUTENAME, --substitutename=SUBSTITUTENAME
|
||||
Use a different name other than the nodename for ping. This may be an
|
||||
expression, such as {bmc} or, if no { character is present, it is treated as a suffix. -s -eth1 would make n1 become n1-eth1, for example.
|
||||
Use a different name other than the nodename for ping. This may be a
|
||||
expression, such as {bmc} or, if no { character is present, it is treated as a suffix. -s -eth1 would make n1 become n1-eth1, for example.
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user