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

Compare commits

..

61 Commits

Author SHA1 Message Date
Jarrod Johnson a9ba385996 Fix type of maxnodes 2026-08-21 14:58:13 -04:00
Jarrod Johnson 29a9d417d6 Do not aggressively respawn buffer daemon.
Start buffer daemon only when needed.

Limit restarts to once every 30 seconds.
2026-08-20 07:23:40 -04:00
Jarrod Johnson 7347125b6f Disable implicit tenant creation
If we support more tenants, we will modify that branch.
2026-07-16 12:39:51 -04:00
Jarrod Johnson 66347a937e Ensure prefix is a string 2026-07-14 11:01:32 -04:00
Jarrod Johnson ee01dfd05e Add UEFI HTTP boot for arm64 to recognized archs
For now, we serve up the whole image, no need to distinguish arm from x86 here yet
2026-07-14 09:52:27 -04:00
Jarrod Johnson 611e1f5bc3 Fix nodemedia attach
The hardening blocked all URL patters.
2026-07-14 09:48:09 -04:00
Jarrod Johnson 7622145fea Fall back to x64 if the aarch64 location didn't pan out 2026-07-13 10:01:04 -04:00
Jarrod Johnson 678bd53857 Correct paths to aarch64 boot material in imgutil 2026-07-13 09:23:33 -04:00
Jarrod Johnson 3295778566 Improve webauthn error handling and tighten up routing
If not webauthn, have authorize
bail out on webauthn request instead
of a sessionless authdata.

For some "special" HTTP paths, tighten up routing rules.
2026-07-08 12:03:48 -04:00
Jarrod Johnson b3acf011bd Restart vtbufferd on exit
Notably, if you strace it, it will trigger an exit(1).  There was at least one documented segmentation fault as well.

Buffer content is lost in such an event, but service remains running.
2026-07-02 12:20:01 -04:00
Jarrod Johnson dc4cafc29f Rework autoconsole logic
Match autocons

Skip unless EFI x86_64.

If SPCR, trust it and use that unconditionally.

Otherwise, if only one can respond to TIOCMGET, then use that one.

If multiple can respond, but exactly one shows carrier, use that.
2026-06-25 16:41:32 -04:00
Jarrod Johnson 3340585fb4 Only count copernicus replies that have OK status 2026-06-25 15:24:31 -04:00
Jarrod Johnson 4456767122 When possible, check confluent user access to file
If a confluent user is a system user, do not allow them to
upload paths that their user would not have access to otherwise.

For non-system users, continue with the path based banned behavior.
2026-06-25 12:15:13 -04:00
Jarrod Johnson 556bb1d0ff Prevent staging of files from indicating path traversal 2026-06-25 10:09:59 -04:00
Jarrod Johnson a201e9886e Have messages force normalizing the incoming filenames
This avoids downstream code that may expect specific locations from being confused.
2026-06-25 09:48:40 -04:00
Jarrod Johnson f82993efe7 Fix debian deployment on slow network link up
When network link was slow to establish, it would fall right through
the network initilalization code.

Now keep working it until a result is acheived.
2026-06-25 08:37:19 -04:00
Jarrod Johnson f8366a50ef Do not set 0.0.0.0 gateway 2026-06-24 15:59:10 -04:00
Jarrod Johnson d369dcac55 Fix non-bonding configuration of ubuntu 2026-06-16 12:42:03 -04:00
Jarrod Johnson 3b2d92a219 Correct typo in pthread name 2026-06-08 11:00:40 -04:00
Jarrod Johnson ffacb66c62 Update rdma vintage 2026-06-08 10:56:26 -04:00
Jarrod Johnson 2073f421da Add libraries to genesis 2026-06-08 10:54:50 -04:00
Jarrod Johnson ed2eed66dc Allow nodedeploy to request http boot specifically 2026-06-03 09:28:44 -04:00
Jarrod Johnson d8f9b6c8e6 Add support for http boot
Some redfish require us to be very specific.
2026-06-03 09:12:46 -04:00
Jarrod Johnson d9a18a7bf6 Actually use the interposer for firmware update 2026-06-01 19:52:43 -04:00
Markus Hilger 439a930188 confignet: Fix interface type detection for IB VFs
IB VFs have the following "ip l" output:

4: ibp129s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 2044 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/infiniband 00:00:00:8d:fe:80:00:00:00:00:00:00:60:5e:65:03:00:2c:43:c8 brd 00:ff:ff:ff:ff:12:40:1b:ff:ff:00:00:00:00:00:00:ff:ff:ff:ff
    vf 0     link/infiniband 00:00:00:8d:fe:80:00:00:00:00:00:00:60:5e:65:03:00:2c:43:c8 brd 00:ff:ff:ff:ff:12:40:1b:ff:ff:00:00:00:00:00:00:ff:ff:ff:ff, spoof checking off, NODE_GUID 00:00:00:00:00:00:00:00, PORT_GUID 00:00:00:00:00:00:00:00, link-state enable, trust off, query_rss off
5: eno1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
    link/ether 30:56:0f:17:c0:b4 brd ff:ff:ff:ff:ff:ff
    altname enp196s0
    altname enx30560f17c0b4

This breaks the detection script because index 0 of the "vf 0 ..." line is not link/<type> anymore.
This commit improves the detection logic to fix this.
2026-06-01 19:30:54 -04:00
Jarrod Johnson 00b2afd42b Fixes for confignet for Ubuntu
Try to find various layers of network config and normalize.

Ultimately, after post subiquity will do some things and easiest to fix in firstboot instead.
2026-06-01 16:49:42 -04:00
Jarrod Johnson 90c2a4cf73 Fix iterating the netplan configuration 2026-06-01 12:55:11 -04:00
Jarrod Johnson c691dc7159 Remove cloud-init netplan if redundant 2026-06-01 09:33:25 -04:00
Jarrod Johnson a7c188b812 Add support for passing a parameterfile in updates 2026-06-01 07:51:22 -04:00
Jarrod Johnson 5ba43ecaa0 Add bonding to netplan management 2026-05-26 10:06:49 -04:00
Vinícius Ferrão 2f53d3bde6 Include xen-front drivers in confluent-curated initramfs 2026-05-23 14:21:13 -04:00
Jarrod Johnson 9fe9e8672a Support more states 2026-05-21 10:03:08 -04:00
Jarrod Johnson 0fe60175f3 Add missing close 2026-05-21 08:36:16 -04:00
Jarrod Johnson d411041243 Recognize more storage states 2026-05-20 16:14:24 -04:00
Jarrod Johnson cc101d12bc Port diskless enhancements from el9 to ubuntu 2026-05-20 15:30:23 -04:00
Jarrod Johnson 2f08ee81f2 Fix off by one in urlmount 2026-05-20 12:37:36 -04:00
Jarrod Johnson 1e9231eea6 Add megasol method 2026-05-19 09:17:50 -04:00
Jarrod Johnson 24cb05e535 Fix name of ssh in various ubuntu scripts 2026-05-13 13:52:05 -04:00
Jarrod Johnson 72b95abb4f Add missing syncfiles examples to ubuntu profiles 2026-05-13 13:51:59 -04:00
Jarrod Johnson 57a170d0d8 Implement a headless mode
For automation, this can make more sense.
2026-05-12 11:23:37 -04:00
Jarrod Johnson daeabc6fe5 Add expression support to the nodeconsole automation 2026-05-11 16:51:01 -04:00
Jarrod Johnson 28a8f6f0d6 Provide automation facility for nodeconsole
Allow nodeconsole to walk console according to a script
2026-05-11 13:55:54 -04:00
Jarrod Johnson 7542897b43 Normalize perms to int or None 2026-05-07 09:44:00 -04:00
Jarrod Johnson c69952265f Permit override of unix ownership/permissions on sockets
If an environment knows more specifically what should have access in terms of group, allow service.cfg to indicate.
2026-05-07 08:44:16 -04:00
Jarrod Johnson 01cc86fa55 Add a '-r' argument to refresh site contents
If an environment manually manages all materials,
provide -r to let
them request packing of those materials
without trying to generate any of the content.
2026-05-06 08:46:31 -04:00
Jarrod Johnson d6e3c7e837 Backport cert fix 2026-05-05 16:26:49 -04:00
Jarrod Johnson dcb6aeca65 Add ca-only policy
This policy forces CA validation every time.

This also checks things like date validity.
2026-05-05 14:41:02 -04:00
Jarrod Johnson 7bc76b62e6 Backport CA policy changes 2026-05-05 11:31:26 -04:00
Jarrod Johnson db313628c5 Include aarch64 names for key libraries in ubuntu diskless 2026-05-01 14:25:18 -04:00
Jarrod Johnson f260323d2f Fix missing ubuntu diskless content 2026-05-01 12:14:13 -04:00
Jarrod Johnson d60bc7f524 Bring chrony fixes to other scripts 2026-04-30 11:24:20 -04:00
Jarrod Johnson ff0d4cdadf Fix diskless profiles for chrony.conf modification 2026-04-30 11:24:15 -04:00
Timothy Middelkoop db6475c4da Fix el8/el9 hook paths corrupted by symlinked el10 in aarch64 spec
In confluent_osdeploy-aarch64.spec.tmpl, el10 was created as a symlink
to el8, so the subsequent `mv el10/initramfs/usr el10/initramfs/var`
inadvertently renamed el8's usr directory, leaving el8 and el9 (also
symlinked to el8) with hooks at var/lib/dracut/hooks/ instead of
usr/lib/dracut/hooks/. Rocky 9 dracut never found the hooks and dropped
to the emergency shell on all aarch64 nodes.

Use `cp -a el8 el10` as the x86_64 spec already does, so the rename
only affects the el10 copy.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Timothy Middelkoop <tmiddelkoop@internet2.edu>
2026-04-30 08:09:06 -04:00
Jarrod Johnson 6d27e8a009 Allow monitor to read attributes by 'all' resource. 2026-04-29 07:51:08 -04:00
Jarrod Johnson f363796439 Write to stdout as binary
This allows better redirection.

In python3, must write to sys.stdout.buffer.  AttributeError for the unlikely event of a python2 based node being deployed.
2026-04-29 07:45:49 -04:00
Jarrod Johnson dec118a985 Fix mistake in spec file 2026-04-24 09:29:31 -04:00
Jarrod Johnson 38eb0d7b10 Add Ubuntu 26.04 2026-04-24 08:35:44 -04:00
xu_ren_xian ae338daa43 Handle confluent= boot arg and IPv4 NIC autodetect
Add support for a confluent=<host> kernel argument in init-premount: configure networking, flush interfaces, autodetect the primary NIC (saved to /tmp/autodetectnic), verify TLS connectivity to the provided server, call the whoami endpoint over TLS to obtain the node name, and write results to /custom-installation/confluent/confluent.info (with fallback to copernicus on failure).

Also update casper-bottom logic to handle IPv4 manager addresses: for IPv6 the manager is still bracketed and scoped interface resolved as before; for IPv4 the script now uses the previously detected NIC (/tmp/autodetectnic) or falls back to an `ip route get <mgr>` lookup to determine DEVICE. This ensures routed IPv4 deployments work correctly.
2026-04-23 17:50:41 -04:00
Jarrod Johnson 6ad3f0d70c Fix mistakes in the node apoption samples 2026-04-20 09:46:45 -04:00
Jarrod Johnson c0b9bb3ab1 Fix group rename in collective 2026-04-17 11:57:35 -04:00
Jarrod Johnson b32755b0d3 Fix remote client operation with Python 3.12+ 2026-04-17 09:01:12 -04:00
461 changed files with 15720 additions and 50363 deletions
-127
View File
@@ -1,127 +0,0 @@
name: CI
on:
push:
pull_request:
permissions:
contents: read
jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run ShellCheck (errors only)
# Check every tracked file that has a .sh extension or an sh/bash
# shebang. SC2148 (missing shebang) is excluded because many .sh
# files are sourced fragments or dracut hooks; ShellCheck then
# falls back to checking them as bash.
run: |
shebang_re='^#!.*[/ ](sh|bash|dash|ash|ksh)([[:blank:]]|$)'
{
git ls-files '*.sh'
git ls-files | while IFS= read -r f; do
[ -f "$f" ] || continue
firstline=
# An empty file makes read fail, which under -e would end the run.
IFS= read -r -n 200 firstline < "$f" 2>/dev/null || true
if [[ $firstline =~ $shebang_re ]]; then
printf '%s\n' "$f"
fi
done
} | sort -u > /tmp/shfiles
# A selection that quietly comes up empty would check nothing and
# still pass, so say how many files there are and insist on some.
echo "$(wc -l < /tmp/shfiles) shell files"
[ -s /tmp/shfiles ] || { echo '::error::No shell files found'; exit 1; }
xargs -d '\n' shellcheck --severity=error --exclude=SC2148 < /tmp/shfiles
ruff:
name: Ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Pinned to an exact release: unlike actions/checkout, ruff-action
# publishes no moving major tag past v3, so @v4 does not resolve.
- uses: astral-sh/ruff-action@v4.1.0
with:
version: latest
# Rule selection, file discovery (the many extensionless Python
# executables) and exclusions all live in ruff.toml, so the whole
# workspace can be handed over as-is.
args: check --output-format=github
pyrefly:
name: Pyrefly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: facebook/pyrefly@main
python-compileall:
name: Python compileall
runs-on: ubuntu-latest
env:
# One entry per Python version shipped by the distros confluent
# targets, limited to versions actions/setup-python still provides
# on current runners (sles15/alma8 ship 3.6, which is unavailable).
# Newline-separated so it feeds both setup-python (multiline input)
# and the shell loop below (word-split on whitespace).
PYTHON_VERSIONS: |
3.8
3.9
3.10
3.12
3.13
3.14
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSIONS }}
- name: List the Python files without a .py name
# compileall only ever compiles *.py: handed anything else, even by
# name, it skips it and still exits 0. That leaves every extensionless
# CLI tool, deploy script and setup.py.tmpl unchecked, so collect them
# here and feed them to py_compile, which does compile what it is
# given. The first line is read with the shell builtin rather than
# forking head and grep per file.
run: |
shebang_re='^#!.*python'
{
git ls-files | while IFS= read -r f; do
[ -f "$f" ] || continue
case "$f" in *.py) continue ;; esac
firstline=
# An empty file makes read fail, which under -e would end the run.
IFS= read -r -n 200 firstline < "$f" 2>/dev/null || true
if [[ $firstline =~ $shebang_re ]]; then
printf '%s\n' "$f"
fi
done
# Python that carries no shebang at all, so nothing can detect it.
# Kept in step with extend-include in ruff.toml.
git ls-files '*/setup.py.tmpl' '*/scripts/configbmc' \
'*/scripts/add_local_repositories' 'misc/filterpasswd'
} | sort -u > /tmp/pyfiles
# An empty list would leave py_compile with nothing to do and the
# job green, which is the very hole this step exists to close.
echo "$(wc -l < /tmp/pyfiles) files without a .py name"
[ -s /tmp/pyfiles ] || { echo '::error::No such files found'; exit 1; }
- name: Compile all Python files
run: |
rc=0
for v in $PYTHON_VERSIONS; do
echo "::group::Python $v"
ok=0
"python$v" -W error -m compileall -q -x '/\.git/' . || ok=1
xargs -d '\n' "python$v" -W error -m py_compile < /tmp/pyfiles || ok=1
echo "::endgroup::"
if [ "$ok" -ne 0 ]; then
echo "::error::Python $v compileall failed"
rc=1
fi
done
exit "$rc"
-19
View File
@@ -1,19 +0,0 @@
name: Create Release
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Create Release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
+5 -25
View File
@@ -5,10 +5,11 @@
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.
Check [this page](https://hpc.lenovo.com/users/documentation/whatisconfluent.html
) 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/).
If you're coming from xCAT, check out [this comparison](https://hpc.lenovo.com/users/documentation/confluentvxcat.html).
# Documentation
@@ -16,9 +17,9 @@ Confluent documentation is hosted on: https://xcat2.github.io/confluent-docs/
# Download
Get the latest version from: https://xcat2.github.io/confluent-docs/downloads/
Get the latest version from: https://hpc.lenovo.com/users/downloads/
Check release notes on: https://xcat2.github.io/confluent-docs/release_notes/
Check release notes on: https://hpc.lenovo.com/users/news/
# Open Source License
@@ -27,24 +28,3 @@ Confluent is made available under the Apache 2.0 license: https://opensource.org
# Developers
Want to help? Submit a [Pull Request](https://github.com/xcat2/confluent/pulls).
## Versioning and releases
The top-level `VERSION` file names the release the current branch is working toward. Build scripts
call `./mkversion`, which turns it into the version stamped on packages: the tag itself on a release
tag (`4.0.1`), otherwise a development version (`4.0.1.dev5+gdeadbee`, or `4.0.1~dev5+gdeadbee` for
the packages that have no `setup.py`).
`mkversion` also derives a version from the newest tag reachable from HEAD and uses whichever is
higher, so forgetting to bump `VERSION` after tagging the current branch cannot walk the version
backwards. Staying ahead of tags on *other* branches is what `VERSION` itself is for: patch releases
are tagged on release branches, which master never sees.
Cutting a new series:
1. Land the release commit on master and tag `X.Y.0` there.
2. Create branch `X.Y` from that tag; it inherits `VERSION=X.Y.0`.
3. Only then bump `VERSION` on master. Doing it before step 2 leaves the release branch on the
wrong series.
Patch releases land on branch `X.Y` and are tagged `X.Y.Z` there; no `VERSION` edit is needed.
-1
View File
@@ -1 +0,0 @@
4.0.0
+1 -1
View File
@@ -3,7 +3,7 @@ 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 ["apt-get", "install", "-y", "reprepro", "python3-stdeb", "gnupg-agent", "devscripts", "debhelper", "libsoap-lite-perl", "libdbi-perl", "quilt", "git", "python3-pyparsing", "python3-dnspython", "python3-eventlet", "python3-netifaces", "python3-paramiko", "dh-python", "libjson-perl", "ronn", "alien", "gcc", "make"]
RUN ["mkdir", "-p", "/sources/git/"]
RUN ["mkdir", "-p", "/debs/"]
RUN ["mkdir", "-p", "/apt/"]
-11
View File
@@ -1,11 +0,0 @@
FROM ubuntu:resolute
ADD stdeb.patch /tmp/
ADD buildapt.sh /bin/
ADD distributions.tmpl /bin/
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "reprepro", "python3-stdeb", "gnupg-agent", "devscripts", "debhelper", "libsoap-lite-perl", "libdbi-perl", "quilt", "git", "python3-pyparsing", "python3-dnspython", "python3-netifaces", "python3-asyncssh", "dh-python", "libjson-perl", "pandoc", "alien", "gcc", "make"]
RUN ["mkdir", "-p", "/sources/git/"]
RUN ["mkdir", "-p", "/debs/"]
RUN ["mkdir", "-p", "/apt/"]
CMD ["/bin/bash", "/bin/buildapt.sh"]
-21
View File
@@ -1,21 +0,0 @@
#cp -a /sources/git /tmp
for builder in $(find /sources/git -name builddeb); do
cd $(dirname $builder)
./builddeb /debs/
done
cp /prebuilt/* /debs/
cp /osd/*.deb /debs/
mkdir -p /apt/conf/
CODENAME=$(grep VERSION_CODENAME= /etc/os-release | sed -e 's/.*=//')
if [ -z "$CODENAME" ]; then
CODENAME=$(grep VERSION= /etc/os-release | sed -e 's/.*(//' -e 's/).*//')
fi
if ! grep $CODENAME /apt/conf/distributions; then
sed -e s/#CODENAME#/$CODENAME/ /bin/distributions.tmpl >> /apt/conf/distributions
fi
cd /apt/
reprepro includedeb $CODENAME /debs/*.deb
for dsc in /debs/*.dsc; do
reprepro includedsc $CODENAME $dsc
done
-7
View File
@@ -1,7 +0,0 @@
Origin: Lenovo HPC Packages
Label: Lenovo HPC Packages
Codename: #CODENAME#
Architectures: amd64 source
Components: main
Description: Lenovo HPC Packages
-34
View File
@@ -1,34 +0,0 @@
diff -urN t/usr/lib/python3/dist-packages/stdeb/cli_runner.py t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py
--- t/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:30:13.930328999 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:32:05.392731405 +0000
@@ -8,7 +8,7 @@
from ConfigParser import SafeConfigParser # noqa: F401
except ImportError:
# python 3.x
- from configparser import SafeConfigParser # noqa: F401
+ from configparser import ConfigParser # noqa: F401
from distutils.util import strtobool
from distutils.fancy_getopt import FancyGetopt, translate_longopt
from stdeb.util import stdeb_cmdline_opts, stdeb_cmd_bool_opts
diff -urN t/usr/lib/python3/dist-packages/stdeb/util.py t.patch/usr/lib/python3/dist-packages/stdeb/util.py
--- t/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:32:53.864776149 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:33:02.063952870 +0000
@@ -730,7 +730,7 @@
example.
"""
- cfg = ConfigParser.SafeConfigParser()
+ cfg = ConfigParser.ConfigParser()
cfg.read(cfg_files)
if cfg.has_section(module_name):
section_items = cfg.items(module_name)
@@ -801,7 +801,7 @@
if len(cfg_files):
check_cfg_files(cfg_files, module_name)
- cfg = ConfigParser.SafeConfigParser(cfg_defaults)
+ cfg = ConfigParser.ConfigParser(cfg_defaults)
for cfg_file in cfg_files:
with codecs.open(cfg_file, mode='r', encoding='utf-8') as fd:
cfg.readfp(fd)
+2 -22
View File
@@ -24,7 +24,6 @@ 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 +51,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 +68,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,
@@ -145,5 +125,5 @@ while fullline:
if printpending:
if clearpending:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current(options.tabular)
print_current()
+21 -37
View File
@@ -41,6 +41,7 @@
# esc-( would interfere with normal esc use too much
# ~ I will not use for now...
import math
import getpass
import optparse
import os
@@ -107,8 +108,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,15 +131,6 @@ 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:
@@ -160,10 +152,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 +173,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 +453,14 @@ 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]
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
startrequest['parameters']['width'] = width
startrequest['parameters']['height'] = height
for param in argv[2:]:
@@ -624,7 +616,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})
@@ -673,10 +665,9 @@ def get_session_node(shellargs):
return targ, 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) == 4 and args[0] == 'nodes' and 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]
@@ -1042,7 +1033,7 @@ def main():
except IOError:
pass
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
if powerstate is None:
if powerstate == None:
powerstate = True
powertime = time.time()
check_power_state()
@@ -1205,21 +1196,14 @@ def consume_termdata(fh, bufferonly=False):
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):
-567
View File
@@ -1,567 +0,0 @@
#!/usr/bin/python3
# Generate a dnsmasq static DHCP configuration (a file under /etc/dnsmasq.d)
# from the confluent attribute database. One "dhcp-host=" reservation is
# emitted for every network of every node that has a hardware (MAC) address
# defined (net.hwaddr, net.<iface>.hwaddr, net.bmc.hwaddr, ...).
import argparse
import ipaddress
import json
import os
import re
import signal
import subprocess
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
TARGET = '/etc/dnsmasq.d/confluent-dhcp.conf'
TAGPREFIX = 'cdhcp'
# Invocation flags that do not affect the generated content; excluded from the
# recorded "Regenerate:" line and from the settings-changed comparison.
NONCONFIG_ARGS = {'-y', '--yes', '-n', '--stdout'}
def split_list(val):
"""Split a confluent comma/whitespace delimited attribute value."""
if not val:
return []
return [v for v in re.split(r'[\s,]+', val.strip()) if v]
def config_args(argv):
"""Join the invocation arguments that affect the generated content."""
return ' '.join(a for a in argv if a not in NONCONFIG_ARGS)
def parse_net_attrib(attrib):
"""Split a net.* attribute into (network_name, field).
The field is always the final dotted component; everything between
'net.' and the field is the network name, which may itself span
several dotted components.
"""
parts = attrib.split('.')
field = parts[-1]
if len(parts) == 2:
currnet = None
else:
currnet = '.'.join(parts[1:-1])
return currnet, field
def subnet_sort_key(netaddr):
try:
return tuple(int(o) for o in netaddr.split('.'))
except ValueError:
return (0, 0, 0, 0)
def parse_routes():
"""Parse `ip --json route` into a list of (network, route_dict) tuples for
routes with a concrete destination prefix (default routes and the like are
skipped). Returns [] if iproute2 or its JSON output is unavailable."""
try:
out = subprocess.check_output(['ip', '--json', 'route'],
stderr=subprocess.DEVNULL)
except (OSError, subprocess.SubprocessError):
return []
try:
entries = json.loads(out.decode('utf-8', 'replace'))
except ValueError:
return []
routes = []
for route in entries:
dst = route.get('dst')
if not dst or dst == 'default':
continue
if '/' not in dst:
dst += '/32'
try:
net = ipaddress.ip_network(dst, strict=False)
except ValueError:
continue
routes.append((net, route))
return routes
def connected_network_for_ip(routes, ipstr):
"""Most specific directly-attached (prefsrc-bearing) route network that
contains `ipstr`, or None. Used to fill in a missing prefix length."""
try:
ip = ipaddress.ip_address(ipstr)
except ValueError:
return None
best = None
for net, route in routes:
if 'prefsrc' not in route:
continue
if ip in net and (best is None or net.prefixlen > best.prefixlen):
best = net
return best
def prefsrc_for_network(routes, network):
"""This host's preferred source address on `network` per the routing table
(the address dnsmasq should listen on for that subnet), or None."""
fallback = None
for net, route in routes:
prefsrc = route.get('prefsrc')
if not prefsrc:
continue
if net == network:
return prefsrc
if network.network_address in net:
fallback = prefsrc
return fallback
def existing_regen_args(targetpath):
"""Return the content arguments recorded in an existing config's
'Regenerate:' header line, or None if not present/readable."""
try:
with open(targetpath) as cfg:
for line in cfg:
if line.startswith('# Regenerate:'):
rest = line[len('# Regenerate:'):].strip()
marker = 'confluent2dnsmasq'
if rest.startswith(marker):
return rest[len(marker):].strip()
return rest
if not line.startswith('#'):
break
except OSError:
pass
return None
def warn_conflicts(hostentries):
"""Warn about reservations that conflict with one another: the same
hostname on different IPs, one IP reserved more than once (dnsmasq
refuses to start on a duplicate dhcp-host IP), or one MAC reserved
more than once. Diagnostics only; every entry is still emitted."""
byname = {}
byip = {}
bymac = {}
for e in hostentries:
if e['hostname']:
prev = byname.setdefault(e['hostname'], e)
if prev is not e and prev['ip'] != e['ip']:
sys.stderr.write(
"WARNING: hostname '{0}' maps to both {1} ({2}) and "
'{3} ({4})\n'.format(
e['hostname'], prev['ip'], prev['node'],
e['ip'], e['node']))
prev = byip.setdefault(e['ip'], e)
if prev is not e:
sys.stderr.write(
'WARNING: address {0} is reserved for both {1} ({2}) and '
'{3} ({4}); dnsmasq refuses to start on a duplicate '
'dhcp-host IP address\n'.format(
e['ip'], ','.join(prev['macs']), prev['node'],
','.join(e['macs']), e['node']))
for mac in e['macs']:
prev = bymac.setdefault(mac.lower(), e)
if prev is not e:
sys.stderr.write(
'WARNING: MAC {0} is reserved for both {1} ({2}) and '
'{3} ({4})\n'.format(
mac, prev['ip'], prev['node'],
e['ip'], e['node']))
def main():
ap = argparse.ArgumentParser(
description='Create /etc/dnsmasq.d static DHCP reservations for a '
'noderange from the confluent database')
ap.add_argument('noderange',
help='Noderange to generate DHCP reservations for')
ap.add_argument('--target', default=TARGET, metavar='PATH',
help='Output configuration file (default: %(default)s).')
ap.add_argument('--tag-prefix', default=TAGPREFIX, metavar='PREFIX',
help='Prefix for generated dnsmasq tag names '
'(default: %(default)s).')
ap.add_argument('--no-bind-dynamic', dest='emit_bind_dynamic',
action='store_false',
help="Do not emit the 'bind-dynamic' line "
'(it is emitted by default).')
ap.add_argument('--no-listen-address', dest='detect_listen',
action='store_false',
help='Do not autodetect and emit a listen-address line for '
"this host's address on each managed subnet. By "
'default they are emitted together with localhost '
'(127.0.0.1 and ::1).')
ap.add_argument('--no-range', dest='emit_range', action='store_false',
help='Do not emit dhcp-range lines (manage ranges '
'elsewhere). Note: dnsmasq will not answer DHCP on a '
'subnet that has no covering dhcp-range.')
ap.add_argument('--lease', default='24h', metavar='TIME',
help='Lease time for emitted dhcp-range lines '
'(default: 24h; e.g. 1h, 24h, infinite). '
'Pass an empty string to omit it.')
ap.add_argument('-n', '--stdout', action='store_true',
help='Write the configuration to stdout instead of '
'writing the target file.')
ap.add_argument('-y', '--yes', dest='assume_yes', action='store_true',
help='Do not prompt for confirmation when an existing '
'config was generated with different settings.')
ap.add_argument('--allow-empty', action='store_true',
help='Write the file even if no reservations were '
'produced (default: refuse, to avoid clobbering an '
'existing config on an empty/failed read).')
# Optional extra data injected into the DHCP reply, sourced from the
# confluent database. All are OFF by default and scoped per-subnet.
grp = ap.add_argument_group(
'optional DHCP reply data (sourced from confluent, off by default)')
grp.add_argument('--all-options', action='store_true',
help='Enable every optional reply datum listed below.')
grp.add_argument('--gateway', '--router', dest='opt_gateway',
action='store_true',
help='router, option 3, from net.<net>.ipv4_gateway')
grp.add_argument('--dns', dest='opt_dns', action='store_true',
help='dns-server, option 6, from dns.servers')
grp.add_argument('--domain', dest='opt_domain', action='store_true',
help='domain-name, option 15, from dns.domain')
grp.add_argument('--ntp', dest='opt_ntp', action='store_true',
help='ntp-server, option 42, from ntp.servers')
grp.add_argument('--mtu', dest='opt_mtu', action='store_true',
help='interface-mtu, option 26, from net.<net>.mtu')
args = ap.parse_args()
if args.all_options:
args.opt_gateway = True
args.opt_dns = True
args.opt_domain = True
args.opt_ntp = True
args.opt_mtu = True
c = client.Command()
# Kernel routing table (ip --json route), used to fill in a missing prefix
# length on a node address and to pick this host's listen-address (prefsrc)
# for each managed subnet.
routes = parse_routes()
# Node level attributes
domainbynode = {}
dnsbynode = {}
ntpbynode = {}
# Per (node, net) network attributes: nets[node][net] = {field: value}
nets = {}
read_error = False
for ent in c.read('/noderange/{0}/attributes/current'.format(
args.noderange)):
if 'error' in ent:
sys.stderr.write(ent['error'] + '\n')
read_error = True
continue
ent = ent.get('databynode', {})
for node in ent:
for attrib in ent[node]:
val = ent[node][attrib]
if not isinstance(val, dict):
continue
val = val.get('value', None)
if val in (None, ''):
continue
if attrib == 'dns.domain':
domainbynode[node] = val
elif attrib == 'dns.servers':
dnsbynode[node] = val
elif attrib == 'ntp.servers':
ntpbynode[node] = val
elif attrib.startswith('net.'):
currnet, field = parse_net_attrib(attrib)
if field not in ('hwaddr', 'ipv4_address',
'ipv4_gateway', 'hostname', 'mtu'):
continue
nets.setdefault(node, {}).setdefault(currnet, {})
nets[node][currnet][field] = val
# Build reservation entries and aggregate per-subnet information.
hostentries = [] # {subnetkey, macs, ip, hostname}
subnets = {} # subnetkey (netaddr, prefixlen) -> aggregate dict
for node in sorted(nets):
netnames = sorted(nets[node], key=lambda n: (n is not None, n or ''))
for currnet in netnames:
slot = nets[node][currnet]
hwaddr = slot.get('hwaddr')
if not hwaddr:
continue
label = currnet if currnet else '(primary)'
netprefix = (currnet + '.') if currnet else ''
addr = slot.get('ipv4_address')
if not addr:
sys.stderr.write(
'{0}: net {1} has a hwaddr but no net.{2}ipv4_address; '
'skipping\n'.format(node, label, netprefix))
continue
# If confluent has no prefix length on the address, borrow it from
# the matching directly-attached route.
if '/' not in addr:
cnet = connected_network_for_ip(routes, addr)
if cnet is not None:
addr = '{0}/{1}'.format(addr, cnet.prefixlen)
ip = addr.split('/', 1)[0]
# Reservation hostname: first token of the per-net hostname,
# else the node name for the primary (unnamed) network, else
# omit it (named net with no hostname).
hostname = None
if slot.get('hostname'):
hostname = split_list(slot['hostname'])[0]
elif currnet is None:
hostname = node
# Derive the subnet (needed for dhcp-range and for scoping
# the optional dhcp-option tags).
subnetkey = None
if '/' in addr:
try:
iface = ipaddress.ip_interface(addr)
except ValueError as e:
sys.stderr.write(
'{0}: net {1} has invalid address {2} ({3}); '
'skipping\n'.format(node, label, addr, e))
continue
network = iface.network
subnetkey = (str(network.network_address), network.prefixlen)
sub = subnets.get(subnetkey)
if sub is None:
tag = '{0}_{1}_{2}'.format(
args.tag_prefix,
str(network.network_address).replace('.', '_'),
network.prefixlen)
sub = {'netmask': str(network.netmask), 'tag': tag,
'prefsrc': prefsrc_for_network(routes, network),
'gateway': set(), 'dns': set(), 'domain': set(),
'ntp': set(), 'mtu': set()}
subnets[subnetkey] = sub
if slot.get('ipv4_gateway'):
sub['gateway'].add(slot['ipv4_gateway'])
if node in dnsbynode:
sub['dns'].add(dnsbynode[node])
if node in domainbynode:
sub['domain'].add(domainbynode[node])
if node in ntpbynode:
sub['ntp'].add(ntpbynode[node])
if slot.get('mtu'):
sub['mtu'].add(slot['mtu'])
elif args.emit_range:
sys.stderr.write(
'{0}: net {1} address {2} has no /prefix; cannot derive a '
'subnet for dhcp-range (add a prefix or use --no-range). '
'Emitting the dhcp-host anyway.\n'.format(
node, label, addr))
hostentries.append({'subnetkey': subnetkey,
'macs': split_list(hwaddr),
'ip': ip, 'hostname': hostname,
'node': node})
warn_conflicts(hostentries)
# Resolve optional per-subnet options from the aggregated values.
def resolve(values, what, subnetkey):
if not values:
return None
if len(values) > 1:
chosen = sorted(values)[0]
sys.stderr.write(
'subnet {0}/{1}: inconsistent {2} across nodes {3}; using '
'{4}\n'.format(subnetkey[0], subnetkey[1], what,
sorted(values), chosen))
return chosen
return next(iter(values))
optionlines = {} # subnetkey -> [ 'option:...,value', ... ]
for subnetkey, sub in subnets.items():
lines = []
if args.opt_gateway:
gw = resolve(sub['gateway'], 'net.ipv4_gateway', subnetkey)
if gw:
lines.append('option:router,{0}'.format(gw))
if args.opt_dns:
dv = resolve(sub['dns'], 'dns.servers', subnetkey)
if dv:
lines.append('option:dns-server,{0}'.format(
','.join(split_list(dv))))
if args.opt_domain:
dom = resolve(sub['domain'], 'dns.domain', subnetkey)
if dom:
lines.append('option:domain-name,{0}'.format(dom))
if args.opt_ntp:
nv = resolve(sub['ntp'], 'ntp.servers', subnetkey)
if nv:
lines.append('option:ntp-server,{0}'.format(
','.join(split_list(nv))))
if args.opt_mtu:
mv = resolve(sub['mtu'], 'net.mtu', subnetkey)
if mv:
lines.append('option:mtu,{0}'.format(mv))
if lines:
optionlines[subnetkey] = lines
orderedsubnets = sorted(subnets,
key=lambda k: subnet_sort_key(k[0]) + (k[1],))
# Render.
out = ['# Managed by confluent2dnsmasq -- DO NOT EDIT BY HAND.',
'# Regenerate: confluent2dnsmasq {0}'.format(
config_args(sys.argv[1:])),
'# Generated {0}'.format(time.strftime('%Y-%m-%d %H:%M:%S %z')),
'']
if args.emit_bind_dynamic:
out.append('# Allow confluent and dnsmasq to share the same network '
'for DHCP')
out.append('bind-dynamic')
out.append('')
detected = []
if args.detect_listen:
for subnetkey in orderedsubnets:
prefsrc = subnets[subnetkey].get('prefsrc')
if prefsrc and prefsrc not in detected:
detected.append(prefsrc)
out.append('# Listen on localhost plus this host\'s address on each '
'managed subnet,')
out.append('# so dnsmasq serves these networks alongside confluent '
'and bind-dynamic')
out.append('# (a listen-address line also overrides local-service in '
'dnsmasq.conf).')
out.append('listen-address=127.0.0.1')
out.append('listen-address=::1')
for addr in detected:
out.append('listen-address={0}'.format(addr))
out.append('')
hosts_by_subnet = {}
nosubnet_hosts = []
for e in hostentries:
fields = list(e['macs'])
if e['subnetkey'] in optionlines:
fields.append('set:{0}'.format(subnets[e['subnetkey']]['tag']))
fields.append(e['ip'])
if e['hostname']:
fields.append(e['hostname'])
line = 'dhcp-host={0}'.format(','.join(fields))
if e['subnetkey'] is None:
nosubnet_hosts.append(line)
else:
hosts_by_subnet.setdefault(e['subnetkey'], []).append(line)
for subnetkey in orderedsubnets:
sub = subnets[subnetkey]
netaddr, prefixlen = subnetkey
out.append('# subnet {0}/{1}'.format(netaddr, prefixlen))
if args.emit_range:
rangefields = [netaddr, 'static', sub['netmask']]
if args.lease:
rangefields.append(args.lease)
out.append('dhcp-range={0}'.format(','.join(rangefields)))
for optline in optionlines.get(subnetkey, []):
out.append('dhcp-option=tag:{0},{1}'.format(sub['tag'], optline))
for line in sorted(hosts_by_subnet.get(subnetkey, [])):
out.append(line)
out.append('')
if nosubnet_hosts:
out.append('# reservations with no derivable subnet (address had no '
'/prefix)')
out.extend(sorted(nosubnet_hosts))
out.append('')
text = '\n'.join(out).rstrip('\n') + '\n'
# Warnings about configuration choices that affect coexistence with
# confluent's own DHCP service.
if args.detect_listen and subnets and not detected:
sys.stderr.write(
'WARNING: no local address was found on any managed subnet, so '
'dnsmasq will only listen on localhost. Run this on the dnsmasq '
'host, or set interface/listen-address manually.\n')
if not args.detect_listen:
sys.stderr.write(
'WARNING: listen-address autodetection disabled. For dnsmasq to '
'serve these networks together with confluent and bind-dynamic, '
'set interface, except-interface or listen-address manually, or '
'disable local-service=host in dnsmasq.conf.\n')
if not args.emit_bind_dynamic:
sys.stderr.write(
'WARNING: bind-dynamic not emitted; this can conflict with '
"confluent's DHCP unless bind-dynamic is set elsewhere in the "
'dnsmasq configuration.\n')
if args.stdout:
sys.stdout.write(text)
return
if not hostentries and not args.allow_empty:
sys.stderr.write(
'No DHCP reservations were generated (no networks with both a '
'hwaddr and an ipv4_address were found){0}. Refusing to overwrite '
'{1}; pass --allow-empty to override.\n'.format(
' and there were read errors' if read_error else '',
args.target))
sys.exit(1)
# If a config already exists that was generated with different
# content-affecting settings, confirm before overwriting it.
if os.path.exists(args.target):
prev = existing_regen_args(args.target)
cur = config_args(sys.argv[1:])
if prev is not None and prev != cur:
sys.stderr.write(
'Existing {0} was generated with different settings:\n'
' old: confluent2dnsmasq {1}\n'
' new: confluent2dnsmasq {2}\n'.format(
args.target, prev or '(no arguments)',
cur or '(no arguments)'))
if not args.assume_yes:
if not sys.stdin.isatty():
sys.stderr.write(
'Refusing to regenerate with changed settings '
'non-interactively; pass -y/--yes to confirm.\n')
sys.exit(1)
try:
resp = input('Are you sure you want to regenerate using the new settings? [y/N] ')
except EOFError:
resp = ''
if resp.strip().lower() not in ('y', 'yes'):
sys.stderr.write(
'Aborted; {0} left unchanged.\n'.format(args.target))
sys.exit(1)
targetdir = os.path.dirname(args.target)
targetname = os.path.basename(args.target)
if targetdir and not os.path.isdir(targetdir):
os.makedirs(targetdir, exist_ok=True)
# Write via a hidden temp file in the same directory, then rename: the
# rename is atomic, and the dot-prefix keeps a dnsmasq conf-dir from
# loading the temp file if it scans mid-write.
tmp = os.path.join(targetdir, '.' + targetname + '.tmp')
with open(tmp, 'w') as cfg:
cfg.write(text)
os.replace(tmp, args.target)
sys.stderr.write('Wrote {0} reservation(s) across {1} subnet(s) to {2}\n'
.format(len(hostentries), len(subnets), args.target))
sys.stderr.write('Be sure to restart dnsmasq for the changes to take effect\n')
if __name__ == '__main__':
main()
+3 -11
View File
@@ -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
@@ -192,6 +183,7 @@ def main():
names.append(fqdn)
names = ' '.join(names)
merger.add_entry(ipdb[node][currnet], names)
merger.write_out('/etc/whatnowhosts')
else:
namesbynode = {}
ipbynode = {}
@@ -206,7 +198,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')
+1 -1
View File
@@ -57,7 +57,7 @@ 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
+5 -10
View File
@@ -37,7 +37,6 @@ 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,
@@ -65,8 +64,7 @@ def run_automation(noderange, category, c):
if res.get('complete', False):
del automationbynode[node]
sys.stdout.write('{0}: Automation complete\n'.format(node))
return exitcode
def run():
global devnull
@@ -106,13 +104,10 @@ def run():
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)
run_automation(args[0], options.automation, c)
nodemap = {}
cmdparms = []
@@ -129,7 +124,7 @@ def run():
cmdstorun.append(['run_remote', script])
if not cmdstorun:
if options.automation:
sys.exit(autoexitcode)
sys.exit(0)
argparser.print_help()
sys.exit(1)
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
@@ -150,7 +145,7 @@ def run():
else:
pendingexecs.append((sshnode, cmdv))
if not all or exitcode:
sys.exit(exitcode | autoexitcode)
sys.exit(exitcode)
rdy = poller.poll(10)
while all:
pernodeout = {}
@@ -198,7 +193,7 @@ def run():
sys.stdout.flush()
if all:
rdy = poller.poll(10)
sys.exit(exitcode | autoexitcode)
sys.exit(exitcode)
def run_cmdv(node, cmdv, all, poller, pipedesc):
+114 -119
View File
@@ -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,126 @@ 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)
+2 -2
View File
@@ -40,7 +40,7 @@ argparser.add_option('-m', '--maxnodes', type='int',
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 '
help='Set passwod, but from environment variable of '
'same name')
@@ -92,7 +92,7 @@ for rsp in session.read('/noderange/{0}/configuration/management_controller/user
continue
for user in rsp['databynode'][node]['users']:
if user['username'] == username:
if user['uid'] not in uid_dict:
if not user['uid'] in uid_dict:
uid_dict[user['uid']] = node
continue
uid_dict[user['uid']] = uid_dict[user['uid']] + ',{}'.format(node)
+106 -114
View File
@@ -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
@@ -78,9 +78,6 @@ 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 = {
@@ -173,7 +170,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 +182,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 +222,109 @@ 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
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:
if options.advanced:
rcode |= client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
session, printextbmc, options)
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())
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)
File diff suppressed because it is too large Load Diff
+26 -36
View File
@@ -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)
+9 -15
View File
@@ -2,7 +2,6 @@
import argparse
import datetime
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
@@ -53,7 +52,7 @@ 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': ''}
args = {'deployment.pendingprofile': prof, 'deployment.state': '', 'deployment.state_detail': ''}
if not prof.startswith('genesis-'):
args['deployment.stagedprofile'] = ''
args['deployment.profile'] = ''
@@ -61,7 +60,7 @@ def setpending(nr, profile, profilebynodes, cli):
args):
pass
return
args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': '', 'deployment.state_last_updated': ''}
args = {'deployment.pendingprofile': profile, 'deployment.state': '', 'deployment.state_detail': ''}
if not profile.startswith('genesis-'):
args['deployment.stagedprofile'] = ''
args['deployment.profile'] = ''
@@ -81,7 +80,7 @@ def main(args):
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('-m', '--maxnodes', help='Specifiy a maximum nodes to be deployed')
ap.add_argument('-r', '--redeploy', help='Redeploy nodes with the current or pending profile', action='store_true')
ap.add_argument('noderange', help='Set of nodes to deploy')
ap.add_argument('profile', nargs='?', help='Profile name to deploy')
@@ -133,6 +132,11 @@ def main(args):
curr = nodeinfo[attr].get('value', '')
if curr and node not in profilebynode:
profilebynode[node] = curr
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 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
@@ -175,7 +179,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 +193,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,19 +203,11 @@ 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:
+76 -92
View File
@@ -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
@@ -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
@@ -213,12 +211,12 @@ async def import_csv(options, session):
alldata.append(nodedatum)
allthere = True
for nodedatum in alldata:
if not await search_record(nodedatum, options, session) and not broken:
if not search_record(nodedatum, options, session) and not broken:
allthere = False
await blocking_scan(session)
blocking_scan(session)
break
for nodedatum in alldata:
if not allthere and not await search_record(nodedatum, options, session):
if not allthere and not search_record(nodedatum, options, session):
sys.stderr.write(
"Could not match the following data: " +
repr(nodedatum) + '\n')
@@ -226,13 +224,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 +237,13 @@ 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()
child = os.fork()
if child:
continue
for mac in maclist:
async for res in mysess.update('/discovery/by-id/{0}'.format(mac),
{'node': nodename}):
mysess = client.Command()
for res in mysess.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 +252,17 @@ async def assign_macs(maclist, nodename, assignlimit):
print('Discovered ' + res['assigned'])
else:
print(repr(res))
return exitcode
sys.exit(0)
while True:
try:
os.wait()
except ChildProcessError:
break
if exitcode:
sys.exit(exitcode)
async def list_discovery(options, session):
def list_discovery(options, session):
orderby = None
if options.fields:
columns = []
@@ -288,24 +278,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 +313,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 +340,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 +361,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 +378,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 +392,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 +419,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()
+2 -62
View File
@@ -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,19 +72,6 @@ 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
@@ -144,7 +125,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 +145,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 +158,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 +172,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)
+6 -32
View File
@@ -56,14 +56,11 @@ components = ['all']
argparser = optparse.OptionParser(
usage="Usage: "
"%prog <noderange> [list][updatestatus][updatetypes][update [--backup] "
"[--parameterfile <file>] <file>]|[<components>]")
"%prog <noderange> [list][updatestatus][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')
help='When updating, use the specified parameter file')
argparser.add_option('-m', '--maxnodes', type='int',
help='When updating, prompt if more than the specified '
'number of servers will be affected')
@@ -71,7 +68,6 @@ argparser.add_option('-m', '--maxnodes', type='int',
(options, args) = argparser.parse_args()
upfile = None
querystatus = False
querytypes = False
try:
noderange = args[0]
if len(args) > 1:
@@ -83,8 +79,6 @@ try:
comps = args[2:]
elif args[1] == 'updatestatus':
querystatus = True
elif args[1] == 'updatetypes':
querytypes = True
else:
comps = args[1:]
components = []
@@ -102,7 +96,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:
@@ -146,11 +140,6 @@ def update_firmware(session, filename):
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(
@@ -204,21 +193,7 @@ 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:
@@ -226,13 +201,12 @@ try:
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)
else:
print(repr(res))
elif upfile is None:
show_firmware(session)
else:
+1 -1
View File
@@ -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')
+2 -12
View File
@@ -38,8 +38,6 @@ if sys.version_info[0] < 3:
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
filters = []
wanted = []
matched = False
def pretty(text):
@@ -123,10 +121,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 +203,6 @@ try:
continue
if info[datum] is None:
continue
matched = True
if options.json:
if node not in databynode:
databynode[node] = {}
@@ -217,11 +212,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 -1
View File
@@ -162,7 +162,7 @@ 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))
print('Error: net.{0}.switch attribute is not valid')
continue
path = path_between_nodes(start_switches, end_switches)
print(f'{start_node} to {end_node}: {path}')
+1 -1
View File
@@ -19,6 +19,7 @@ import optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
@@ -119,7 +120,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',
+7 -8
View File
@@ -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,9 +59,9 @@ 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
@@ -74,4 +73,4 @@ async def main():
sys.exit(exitcode)
if __name__ == '__main__':
asyncio.run(main())
main()
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
+23 -30
View File
@@ -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)
+1
View File
@@ -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():
+2 -12
View File
@@ -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,10 +120,6 @@ 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']:
@@ -148,10 +144,7 @@ def sensorpass(showout=True, appendtime=False):
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'')):
if sensedata.get('units', None) 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)
-4
View File
@@ -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',
@@ -121,8 +119,6 @@ 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
-2
View File
@@ -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__':
-9
View File
@@ -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()
-860
View File
@@ -1,860 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import ctypes
import ctypes.util
import dbm
import csv
import errno
import fnmatch
import hashlib
import os
import shlex
import socket
import ssl
import sys
import confluent.asynctlvdata as tlvdata
import confluent.sortutil as sortutil
libssl = ctypes.CDLL(ctypes.util.find_library('ssl'))
libssl.SSL_CTX_set_cert_verify_callback.argtypes = [
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
SO_PASSCRED = 16
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'switchuser': 'secret.hardwaremanagementuser',
'bmcpass': 'secret.hardwaremanagementpassword',
'switchpass': 'secret.hardwaremanagementpassword',
}
try:
getinput = raw_input
except NameError:
getinput = input
class PyObject_HEAD(ctypes.Structure):
_fields_ = [
("ob_refcnt", ctypes.c_ssize_t),
("ob_type", ctypes.c_void_p),
]
# see main/Modules/_ssl.c, only caring about the SSL_CTX pointer
class PySSLContext(ctypes.Structure):
_fields_ = [
("ob_base", PyObject_HEAD),
("ctx", ctypes.c_void_p),
]
@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def verify_stub(store, misc):
return 1
class NestedDict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
def stringify(instr):
# Normalize unicode and bytes to 'str', correcting for
# current python version
if isinstance(instr, bytes) and not isinstance(instr, str):
return instr.decode('utf-8', errors='replace')
elif not isinstance(instr, bytes) and not isinstance(instr, str):
return instr.encode('utf-8')
return instr
class Tabulator(object):
def __init__(self, headers):
self.headers = headers
self.rows = []
def add_row(self, row):
self.rows.append(row)
def get_table(self, order=None):
i = 0
fmtstr = ''
separator = []
for head in self.headers:
if order and order == head:
order = i
neededlen = len(head)
for row in self.rows:
if len(row[i]) > neededlen:
neededlen = len(row[i])
separator.append('-' * (neededlen + 1))
fmtstr += '{{{0}:>{1}}}|'.format(i, neededlen + 1)
i = i + 1
fmtstr = fmtstr[:-1]
yield fmtstr.format(*self.headers)
yield fmtstr.format(*separator)
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
yield fmtstr.format(*row)
else:
for row in self.rows:
yield fmtstr.format(*row)
def write_csv(self, output, order=None):
output = csv.writer(output)
output.writerow(self.headers)
i = 0
for head in self.headers:
if order and order == head:
order = i
i = i + 1
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
output.writerow(row)
else:
for row in self.rows:
output.writerow(row)
def printerror(res, node=None):
exitcode = 0
if 'errorcode' in res:
exitcode = res['errorcode']
for node in res.get('databynode', {}):
exitcode = res['databynode'][node].get('errorcode', exitcode)
if 'error' in res['databynode'][node]:
sys.stderr.write(
'{0}: {1}\n'.format(node, res['databynode'][node]['error']))
if exitcode == 0:
exitcode = 1
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
return exitcode
def cprint(txt):
try:
print(txt)
except UnicodeEncodeError:
print(txt.encode('utf8'))
sys.stdout.flush()
def _parseserver(string):
if ']:' in string:
server, port = string[1:].split(']:')
elif string[0] == '[':
server = string[1:-1]
port = '13001'
elif ':' in string:
server, port = string.split(':')
else:
server = string
port = '13001'
return server, port
class Command(object):
def __init__(self, server=None):
self._prevdict = None
self._prevkeyname = None
self.connection = None
self._currnoderange = None
self.unixdomain = False
if server is None:
if 'CONFLUENT_HOST' in os.environ:
self.serverloc = os.environ['CONFLUENT_HOST']
else:
self.serverloc = '/var/run/confluent/api.sock'
else:
self.serverloc = server
self.connected = False
async def ensure_connected(self):
if self.connected:
return True
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
self._connect_unix()
self.unixdomain = True
elif self.serverloc == '/var/run/confluent/api.sock':
raise Exception('Confluent service is not available')
else:
await self._connect_tls()
self.protversion = int((await tlvdata.recv(self.connection)).split(
b'--')[1].strip()[1:])
authdata = await tlvdata.recv(self.connection)
if authdata['authpassed'] == 1:
self.authenticated = True
else:
self.authenticated = False
if not self.authenticated and 'CONFLUENT_USER' in os.environ:
username = os.environ['CONFLUENT_USER']
passphrase = os.environ['CONFLUENT_PASSPHRASE']
await self.authenticate(username, passphrase)
self.connected = True
async def add_file(self, name, handle, mode):
await self.ensure_connected()
if self.protversion < 3:
raise Exception('Not supported with connected confluent server')
if not self.unixdomain:
raise Exception('Can only add a file to a unix domain connection')
await tlvdata.send(self.connection, {'filename': name, 'mode': mode}, handle)
async def authenticate(self, username, password):
await tlvdata.send(self.connection,
{'username': username, 'password': password})
authdata = await tlvdata.recv(self.connection)
if authdata['authpassed'] == 1:
self.authenticated = True
def add_precede_key(self, keyname):
self._prevkeyname = keyname
def add_precede_dict(self, dict):
self._prevdict = dict
def handle_results(self, ikey, rc, res, errnodes=None, outhandler=None):
if 'error' in res:
if errnodes is not None:
errnodes.add(self._currnoderange)
sys.stderr.write('Error: {0}\n'.format(res['error']))
if 'errorcode' in res:
return res['errorcode']
else:
return 1
if 'databynode' not in res:
return 0
res = res['databynode']
for node in res:
if 'error' in res[node]:
if errnodes is not None:
errnodes.add(node)
sys.stderr.write('{0}: Error: {1}\n'.format(
node, res[node]['error']))
if 'errorcode' in res[node]:
rc |= res[node]['errorcode']
else:
rc |= 1
elif ikey in res[node]:
if 'value' in res[node][ikey]:
val = res[node][ikey]['value']
elif 'isset' in res[node][ikey]:
val = '********' if res[node][ikey] else ''
else:
val = repr(res[node][ikey])
if self._prevkeyname and self._prevkeyname in res[node]:
cprint('{0}: {2}->{1}'.format(
node, val, res[node][self._prevkeyname]['value']))
elif self._prevdict and node in self._prevdict:
cprint('{0}: {2}->{1}'.format(
node, val, self._prevdict[node]))
else:
cprint('{0}: {1}'.format(node, val))
elif outhandler:
outhandler(node, res)
return rc
async def simple_noderange_command(self, noderange, resource, input=None,
key=None, errnodes=None, promptover=None, outhandler=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
async for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
else:
await self.stop_if_noderange_over(noderange, promptover)
kwargs[ikey] = input
async for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
self._currnoderange = None
return rc
except KeyboardInterrupt:
cprint('')
return 0
async def stop_if_noderange_over(self, noderange, maxnodes):
if maxnodes is None:
return
nsize = await self.get_noderange_size(noderange)
if nsize > maxnodes:
if nsize == 1:
nodename = [x async for x in self.read(
'/noderange/{0}/nodes/'.format(noderange))][0].get('item', {}).get('href', None)
nodename = nodename[:-1]
p = getinput('Command is about to affect node {0}, continue (y/n)? '.format(nodename))
else:
p = getinput('Command is about to affect {0} nodes, continue (y/n)? '.format(nsize))
if p.lower() != 'y':
sys.stderr.write('Aborting at user request\n')
sys.exit(1)
raise Exception("Aborting at user request")
async def get_noderange_size(self, noderange):
numnodes = 0
async for node in self.read('/noderange/{0}/nodes/'.format(noderange)):
if node.get('item', {}).get('href', None):
numnodes += 1
else:
raise Exception("Error trying to size noderange {0}".format(noderange))
return numnodes
async def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
try:
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
async for res in self.read('/nodegroups/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
else:
kwargs[ikey] = input
async for res in self.update('/nodegroups/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
cprint('')
return 0
async def read(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'retrieve', path, self.connection, parameters):
yield rsp
async def update(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'update', path, self.connection, parameters):
yield rsp
async def create(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'create', path, self.connection, parameters):
yield rsp
async def delete(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'delete', path, self.connection, parameters):
yield rsp
def _connect_unix(self):
self.connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.connection.setsockopt(socket.SOL_SOCKET, SO_PASSCRED, 1)
self.connection.connect(self.serverloc)
self.connection.setblocking(False)
async def _connect_tls(self):
server, port = _parseserver(self.serverloc)
for res in await asyncio.get_running_loop().getaddrinfo(
server, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
try:
self.connection = socket.socket(af, socktype, proto)
self.connection.setsockopt(
socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except:
self.connection = None
continue
try:
self.connection.settimeout(5)
self.connection.connect(sa)
self.connection.settimeout(0)
except:
raise
self.connection.close()
self.connection = None
continue
break
if self.connection is None:
raise Exception("Failed to connect to %s" % self.serverloc)
#TODO(jbjohnso): server certificate validation
clientcfgdir = os.path.join(os.path.expanduser("~"), ".confluent")
try:
os.makedirs(clientcfgdir)
except OSError as exc:
if not (exc.errno == errno.EEXIST and os.path.isdir(clientcfgdir)):
raise
cacert = os.path.join(clientcfgdir, "ca.pem")
certreqs = ssl.CERT_REQUIRED
knownhosts = False
if not os.path.exists(cacert):
cacert = None
certreqs = ssl.CERT_NONE
knownhosts = True
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ssl_ctx = PySSLContext.from_address(id(ctx)).ctx
libssl.SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_stub, 0)
sreader = asyncio.StreamReader()
sreaderprot = asyncio.StreamReaderProtocol(sreader)
cloop = asyncio.get_running_loop()
tport, _ = await cloop.create_connection(
lambda: sreaderprot, sock=self.connection, ssl=ctx, server_hostname='x')
swriter = asyncio.StreamWriter(tport, sreaderprot, sreader, cloop)
self.connection = (sreader, swriter)
#self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
# cert_reqs=certreqs)
if knownhosts:
certdata = tport.get_extra_info('ssl_object').getpeercert(binary_form=True)
# certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
fingerprint = fingerprint.encode('utf-8')
hostid = '@'.join((port, server))
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
if hostid in khf:
if fingerprint == khf[hostid]:
return
else:
replace = getinput(
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
cprint('Adding new key for %s:%s' % (server, port))
khf[hostid] = fingerprint
async def send_request(operation, path, server, parameters=None):
"""This function iterates over all the responses
received from the server.
:param operation: The operation to request, retrieve, update, delete,
create, start, stop
:param path: The URI path to the resource to operate on
:param server: The socket to send data over
:param parameters: Parameters if any to send along with the request
"""
payload = {'operation': operation, 'path': path}
if parameters is not None:
payload['parameters'] = parameters
await tlvdata.send(server, payload)
result = await tlvdata.recv(server)
while '_requestdone' not in result:
try:
yield result
except GeneratorExit:
while '_requestdone' not in result:
result = await tlvdata.recv(server)
raise
result = await tlvdata.recv(server)
def attrrequested(attr, attrlist, seenattributes, node=None):
for candidate in attrlist:
truename = candidate
if candidate.startswith('hm'):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate in _attraliases:
candidate = _attraliases[candidate]
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = True
return True
elif attr.lower().startswith(candidate.lower() + '.'):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = 1
return True
return False
async def printattributes(session, requestargs, showtype, nodetype, noderange, options):
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
return await print_attrib_path(path, session, requestargs, options)
def _sort_attrib(k):
if isinstance(k[1], dict) and k[1].get('sortid', None) is not None:
return sortutil.naturalize_string('{}'.format(k[1]['sortid']))
return sortutil.naturalize_string(k[0])
def _is_nondefault(currattr):
if currattr.get('default', None) is None:
return False # no default specified cannot tell
if 'value' not in currattr: # can't compare if no value specified
return False
if currattr['value'] != currattr['default']:
return True
# ok, the potentially pending value is the same, but check active
if 'active' not in currattr:
return False
if currattr['active'] != currattr['default']:
return True
return False
async def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None, showpending=False):
exitcode = 0
seenattributes = NestedDict()
allnodes = set([])
async for res in session.read(path):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in sorted(res['databynode']):
allnodes.add(node)
for attr, val in sorted(res['databynode'][node].items(), key=_sort_attrib):
if attr == 'error':
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
continue
if attr == 'errorcode':
exitcode |= val
continue
seenattributes[node][attr] = True
if rename:
printattr = rename.get(attr, attr)
else:
printattr = attr
if attrprefix:
printattr = attrprefix + printattr
currattr = res['databynode'][node][attr]
if show_attr(attr, requestargs, seenattributes, options, node):
if 'value' in currattr:
currval = currattr.get('value', None)
if isinstance(currval, list):
currval = ','.join(currval)
activeval = currattr.get('active', currval)
if isinstance(activeval, list):
activeval = ','.join(activeval)
if activeval != currval:
if currval is None:
currval = ''
if activeval is None:
activeval = ''
if showpending:
attrout = f'{node}: {printattr}: {activeval} -> {currval}'
else:
attrout = f'{node}: {printattr}: {currval} (Pending)'
elif showpending:
continue
elif currval is not None:
attrout = '{0}: {1}: {2}'.format(
node, printattr, currval).strip()
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node,
printattr)
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif isinstance(currattr, dict) and 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(node, printattr,
currattr['broken'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
else:
cprint("CODE ERROR" + repr(attr))
try:
blame = options.blame
except AttributeError:
blame = False
if blame or (isinstance(currattr, dict) and 'broken' in currattr):
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
currattr['inheritedfrom']
))
if 'expression' in currattr:
blamedata.append(
'derived from expression "{0}"'.format(
currattr['expression']))
if blamedata:
attrout += ' (' + ', '.join(blamedata) + ')'
try:
comparedefault = options.comparedefault
except AttributeError:
comparedefault = False
if comparedefault:
try:
exclude = options.exclude
except AttributeError:
exclude = False
if ((requestargs and not exclude) or
_is_nondefault(currattr)):
currval = ','.join(currattr['value']) if isinstance(
currattr['value'], list) else currattr['value']
activeval = ','.join(currattr['active']) if isinstance(
currattr.get('active'), list) else currattr.get('active')
dval = ','.join(currattr['default']) if isinstance(
currattr['default'], list) else currattr['default']
outmsg = '{0}: {1}: {2} (Default: {3}'.format(
node, printattr, currval, dval)
if 'active' in currattr:
outmsg += ', Pending change from: {0}'.format(activeval)
outmsg += ')'
cprint(outmsg)
else:
try:
details = options.detail
except AttributeError:
details = False
if details:
if currattr.get('help', None):
attrout += u' (Help: {0})'.format(
currattr['help'])
if currattr.get('possible', None):
try:
attrout += u' (Choices: {0})'.format(
','.join(currattr['possible']))
except TypeError:
pass
cprint(attrout)
somematched = set([])
printmissing = set([])
badnodes = NestedDict()
if not exitcode:
if requestargs:
for attr in requestargs:
for node in allnodes:
if attr in seenattributes[node]:
somematched.add(attr)
else:
badnodes[node][attr] = True
exitcode = 1
for node in sortutil.natural_sort(badnodes):
for attr in badnodes[node]:
if attr in somematched:
sys.stderr.write(
'Error: {0} matches no valid value for {1}\n'.format(
attr, node))
else:
printmissing.add(attr)
for missing in printmissing:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(missing))
return exitcode
def show_attr(attr, requestargs, seenattributes, options, node):
try:
reverse = options.exclude
except AttributeError:
reverse = False
if requestargs is None or requestargs == []:
return True
processattr = attrrequested(attr, requestargs, seenattributes, node)
if reverse:
processattr = not processattr
return processattr
async def printgroupattributes(session, requestargs, showtype, nodetype, noderange, options):
exitcode = 0
seenattributes = set([])
async for res in session.read('/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for attr in res:
seenattributes.add(attr)
currattr = res[attr]
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
if 'value' in currattr:
if currattr['value'] is not None:
attrout = '{0}: {1}: {2}'.format(
noderange, attr, currattr['value'])
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(noderange, attr)
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif isinstance(currattr, dict) and 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(noderange, attr,
currattr['broken'])
elif 'expression' in currattr:
attrout = '{0}: {1}: (will derive from expression {2})'.format(noderange, attr, currattr['expression'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, dictout)))
else:
cprint("CODE ERROR" + repr(attr))
cprint(attrout)
if not exitcode:
if requestargs:
for attr in requestargs:
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
exitcode = 1
return exitcode
async def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
# update attribute
exitcode = 0
if options.clear:
targpath = '/{0}/{1}/attributes/all'.format(nodetype, noderange)
keydata = {}
for attrib in updateargs[1:]:
keydata[attrib] = None
async for res in session.update(targpath, keydata):
for node in res.get('databynode', {}):
for warnmsg in res['databynode'][node].get('_warnings', []):
sys.stderr.write('Warning: ' + warnmsg + '\n')
if 'error' in res:
if 'errorcode' in res:
exitcode = res['errorcode']
sys.stderr.write('Error: ' + res['error'] + '\n')
sys.exit(exitcode)
elif hasattr(options, 'environment') and options.environment:
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
# Let's do one pass to make sure that there's not a usage problem
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
if (nodetype == "nodegroups"):
exitcode = await session.simple_nodegroups_command(noderange,
'attributes/all',
value, key)
else:
exitcode = await session.simple_noderange_command(noderange,
'attributes/all',
value, key)
sys.exit(exitcode)
elif dictassign:
for key in dictassign:
if nodetype == 'nodegroups':
exitcode = await session.simple_nodegroups_command(
noderange, 'attributes/all', dictassign[key], key)
else:
exitcode = await session.simple_noderange_command(
noderange, 'attributes/all', dictassign[key], key)
else:
if "=" in updateargs[1]:
update_ready = True
for arg in updateargs[1:]:
if '=' not in arg:
update_ready = False
exitcode = 1
if not update_ready:
sys.stderr.write('Error: {0} Can not set and read at the same time!\n'.format(str(updateargs[1:])))
sys.exit(exitcode)
try:
for val in updateargs[1:]:
val = val.split('=', 1)
if val[0][-1] in (',', '-', '^'):
key = val[0][:-1]
if val[0][-1] == ',':
value = {'prepend': val[1]}
elif val[0][-1] in ('-', '^'):
value = {'remove': val[1]}
else:
key = val[0]
value = val[1]
if (nodetype == "nodegroups"):
exitcode = await session.simple_nodegroups_command(noderange, 'attributes/all',
value, key)
else:
exitcode = await session.simple_noderange_command(noderange, 'attributes/all',
value, key)
except Exception:
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
exitcode = 1
sys.exit(exitcode)
return exitcode
# So we try to prevent bad things from happening when globbing
# We tried to head this off at the shell, but the various solutions would end
# up breaking the shell in various ways (breaking pipe capability if using
# DEBUG, breaking globbing if in pipe, etc)
# Then we tried to parse the original commandline instead, however shlex isn't
# going to parse full bourne language (e.g. knowing that '|' and '>' and
# a world of other things would not be in our command line
# so finally, just make sure the noderange appears verbatim in the command line
# if we glob to something, then bash will change noderange and this should
# detect it and save the user from tragedy
def check_globbing(noderange):
if not os.path.exists(noderange):
return True
rawargs = os.environ.get('CURRENT_CMDLINE', None)
if rawargs:
rawargs = shlex.split(rawargs)
for arg in rawargs:
if arg.startswith('$'):
arg = arg[1:]
if arg.endswith(';'):
arg = arg[:-1]
arg = os.environ.get(arg, '$' + arg)
if arg.startswith(noderange):
break
else:
sys.stderr.write(
'Shell glob conflict detected, specified target "{0}" '
'not in command line, but is a file. You can use "set -f" in '
'bash or change directories such that there is no filename '
'that would conflict.'
'\n'.format(noderange))
sys.exit(1)
-313
View File
@@ -1,313 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import array
import asyncio
import ctypes
import ctypes.util
import confluent.tlv as tlv
import socket
from datetime import datetime
import json
import os
import struct
try:
unicode
except NameError:
unicode = str
try:
range = xrange
except NameError:
pass
class iovec(ctypes.Structure): # from uio.h
_fields_ = [('iov_base', ctypes.c_void_p),
('iov_len', ctypes.c_size_t)]
iovec_ptr = ctypes.POINTER(iovec)
class cmsghdr(ctypes.Structure): # also from bits/socket.h
_fields_ = [('cmsg_len', ctypes.c_size_t),
('cmsg_level', ctypes.c_int),
('cmsg_type', ctypes.c_int)]
@classmethod
def init_data(cls, cmsg_len, cmsg_level, cmsg_type, cmsg_data):
Data = ctypes.c_ubyte * ctypes.sizeof(cmsg_data)
class _flexhdr(ctypes.Structure):
_fields_ = cls._fields_ + [('cmsg_data', Data)]
datab = Data(*bytearray(cmsg_data))
return _flexhdr(cmsg_len=cmsg_len, cmsg_level=cmsg_level,
cmsg_type=cmsg_type, cmsg_data=datab)
def CMSG_LEN(length):
sizeof_cmshdr = ctypes.sizeof(cmsghdr)
return ctypes.c_size_t(CMSG_ALIGN(sizeof_cmshdr).value + length)
SCM_RIGHTS = 1
class msghdr(ctypes.Structure): # from bits/socket.h
_fields_ = [('msg_name', ctypes.c_void_p),
('msg_namelen', ctypes.c_uint),
('msg_iov', ctypes.POINTER(iovec)),
('msg_iovlen', ctypes.c_size_t),
('msg_control', ctypes.c_void_p),
('msg_controllen', ctypes.c_size_t),
('msg_flags', ctypes.c_int)]
def CMSG_ALIGN(length): # bits/socket.h
ret = (length + ctypes.sizeof(ctypes.c_size_t) - 1
& ~(ctypes.sizeof(ctypes.c_size_t) - 1))
return ctypes.c_size_t(ret)
def CMSG_SPACE(length): # bits/socket.h
ret = CMSG_ALIGN(length).value + CMSG_ALIGN(ctypes.sizeof(cmsghdr)).value
return ctypes.c_size_t(ret)
class ClientFile(object):
def __init__(self, name, mode, fd):
self.fileobject = os.fdopen(fd, mode)
self.filename = name
def _sendmsg(loop, fut, sock, msg, fds, wfd):
if wfd is not None:
loop.remove_writer(wfd)
if fut.cancelled():
return
try:
retdata = sock.sendmsg(
[msg],
[(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds))])
except (BlockingIOError, InterruptedError):
fd = sock.fileno()
loop.add_writer(fd, _sendmsg, loop, fut, sock, msg, fds, fd)
except Exception as exc:
fut.set_exception(exc)
else:
fut.set_result(retdata)
def send_fds(sock, msg, fds):
cloop = asyncio.get_running_loop()
fut = cloop.create_future()
_sendmsg(cloop, fut, sock, msg, fds, None)
return fut
def _recvmsg(loop, fut, sock, msglen, maxfds, rfd):
if rfd is not None:
loop.remove_reader(rfd)
if fut.cancelled():
return
fds = array.array("i") # Array of ints
try:
msg, ancdata, flags, addr = sock.recvmsg(
msglen, socket.CMSG_LEN(maxfds * fds.itemsize))
except (BlockingIOError, InterruptedError):
fd = sock.fileno()
loop.add_reader(fd, _recvmsg, loop, fut, sock, msglen, maxfds, fd)
except Exception as exc:
fut.set_exception(exc)
else:
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if (cmsg_level == socket.SOL_SOCKET
and cmsg_type == socket.SCM_RIGHTS):
# Append data, ignoring any truncated integers at the end.
fds.frombytes(
cmsg_data[
:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
fut.set_result((msg, list(fds)))
def recv_fds(sock, msglen, maxfds):
cloop = asyncio.get_running_loop()
fut = cloop.create_future()
_recvmsg(cloop, fut, sock, msglen, maxfds, None)
return fut
def decodestr(value):
ret = None
try:
ret = value.decode('utf-8')
except UnicodeDecodeError:
try:
ret = value.decode('cp437')
except UnicodeDecodeError:
ret = value
except AttributeError:
return value
return ret
def unicode_dictvalues(dictdata):
for key in dictdata:
if isinstance(dictdata[key], bytes):
dictdata[key] = decodestr(dictdata[key])
elif isinstance(dictdata[key], datetime):
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(dictdata[key], list):
_unicode_list(dictdata[key])
elif isinstance(dictdata[key], dict):
unicode_dictvalues(dictdata[key])
def _unicode_list(currlist):
for i in range(len(currlist)):
if isinstance(currlist[i], str):
currlist[i] = decodestr(currlist[i])
elif isinstance(currlist[i], dict):
unicode_dictvalues(currlist[i])
elif isinstance(currlist[i], list):
_unicode_list(currlist[i])
async def sendall(handle, data):
if isinstance(handle, tuple):
handle[1].write(data)
return await handle[1].drain()
else:
cloop = asyncio.get_running_loop()
return await cloop.sock_sendall(handle, data)
def get_socket(handle):
if isinstance(handle, tuple):
return handle[1].transport.get_extra_info('socket')
else:
return handle
async def close(handle):
if isinstance(handle, tuple):
handle[1].close()
try:
await handle[1].wait_closed()
except Exception:
pass
else:
handle.close()
async def send(handle, data, filehandle=None):
cloop = asyncio.get_running_loop()
if isinstance(data, unicode):
try:
data = data.encode('utf-8')
except AttributeError:
pass
if isinstance(data, bytes) or isinstance(data, unicode):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
# if you don't have anything to say, don't say anything at all
return
if tl < 16777216:
# type for string is '0', so we don't need
# to xor anything in
await sendall(handle, struct.pack("!I", tl))
else:
raise Exception("String data length exceeds protocol")
await sendall(handle, data)
elif isinstance(data, dict): # JSON currently only goes to 4 bytes
# Some structured message, like what would be seen in http responses
unicode_dictvalues(data) # make everything unicode, assuming UTF-8
sdata = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
sdata = sdata.encode('utf-8')
tl = len(sdata)
if tl > 16777215:
raise Exception("JSON data exceeds protocol limits")
# xor in the type (0b1 << 24)
if filehandle is None:
tl |= 16777216
await sendall(handle, struct.pack("!I", tl))
await sendall(handle, sdata)
elif isinstance(handle, tuple):
raise Exception("Cannot send filehandle over network socket")
else:
tl |= (2 << 24)
await cloop.sock_sendall(handle, struct.pack("!I", tl))
await send_fds(handle, sdata, [filehandle])
async def _grabhdl(handle, size):
if isinstance(handle, tuple):
return await handle[0].read(size)
else:
cloop = asyncio.get_running_loop()
return await cloop.sock_recv(handle, size)
async def recvall(handle, size):
rd = await _grabhdl(handle, size)
while len(rd) < size:
nd = await _grabhdl(handle, size - len(rd))
if not nd:
raise Exception("Error reading data")
rd += nd
return rd
async def recv(handle):
tl = await _grabhdl(handle, 4)
if not tl:
return None
while len(tl) < 4:
ndata = await _grabhdl(handle, 4 - len(tl))
if not ndata:
raise Exception("Error reading data")
tl += ndata
if len(tl) == 0:
return None
tl = struct.unpack("!I", tl)[0]
if tl & 0b10000000000000000000000000000000:
raise Exception("Protocol Violation, reserved bit set")
# 4 byte tlv
dlen = tl & 16777215 # grab lower 24 bits
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
if dlen == 0:
return None
if datatype == tlv.Types.filehandle:
if isinstance(handle, tuple):
raise Exception('Filehandle not supported over TLS socket')
msg, filehandles = await recv_fds(handle, dlen, 4)
data = json.loads(bytes(msg))
return ClientFile(data['filename'], data['mode'], filehandles[0])
else:
data = await _grabhdl(handle, dlen)
while len(data) < dlen:
ndata = await _grabhdl(handle, dlen - len(data))
if not ndata:
raise Exception("Error reading data")
data += ndata
if datatype == tlv.Types.text:
return data
elif datatype == tlv.Types.json:
return json.loads(data)
+24 -64
View File
@@ -280,8 +280,8 @@ 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)
maxnodes = int(maxnodes)
if nsize > maxnodes:
if nsize == 1:
nodename = list(self.read(
@@ -473,21 +473,7 @@ def _sort_attrib(k):
return sortutil.naturalize_string('{}'.format(k[1]['sortid']))
return sortutil.naturalize_string(k[0])
def _is_nondefault(currattr):
if currattr.get('default', None) is None:
return False # no default specified cannot tell
if 'value' not in currattr: # can't compare if no value specified
return False
if currattr['value'] != currattr['default']:
return True
# ok, the potentially pending value is the same, but check active
if 'active' not in currattr:
return False
if currattr['active'] != currattr['default']:
return True
return False
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None, showpending=False):
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None):
exitcode = 0
seenattributes = NestedDict()
allnodes = set([])
@@ -515,26 +501,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 +554,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:
@@ -715,31 +683,23 @@ def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=N
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:
@@ -753,7 +713,7 @@ def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=N
if "=" in updateargs[1]:
update_ready = True
for arg in updateargs[1:]:
if '=' not in arg:
if not '=' in arg:
update_ready = False
exitcode = 1
if not update_ready:
-4
View File
@@ -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())
+3 -47
View File
@@ -16,55 +16,11 @@ import fcntl
import sys
import struct
import termios
import select
def get_screengeom(escfallback=False):
def get_screengeom():
# 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])
return struct.unpack('hhhh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
b'........'))
class ScreenPrinter(object):
def __init__(self, noderange, client, textlen=4):
+2
View File
@@ -138,6 +138,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(
+6 -2
View File
@@ -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
-346
View File
@@ -1,346 +0,0 @@
import asyncio
from PIL import Image
import io
import numpy as np
import queue
import threading
import time
import zlib
# This results in an RGBA organization of pixels
MYPIXFORMAT = bytearray([
32, # bits per pixel
24, # depth
0, # big endian
1, # true color
0, 255, # red max
0, 255, # green max
0, 255, # blue max
0, 8, 16, # red shift, green shift, blue shift
0, 0, 0 # padding
])
class ByteStream:
def __init__(self):
self.buffer = b''
def add_number(self, number, num_bytes):
data = number.to_bytes(num_bytes, byteorder='big', signed=True)
self.buffer += data
def extend(self, data):
self.buffer += data
def get_bytes(self):
return self.buffer
def clear(self):
self.buffer = b''
def flush(self, writer):
writer.write(self.buffer)
self.clear()
class VNCClient:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
return False
@classmethod
async def create(cls, url, outputfile=None, fps=15):
self = cls()
self.outputfile = outputfile
self.fps = fps
self.video_writer = None
self._video_size = None
self._last_frame = None
self._last_frame_time = None
self._video_queue = None
self._video_thread = None
self._cv2 = None
if outputfile:
try:
import cv2
except ImportError:
raise ImportError("OpenCV is required for video output but is not installed.")
self._cv2 = cv2
self._video_queue = queue.Queue()
self._video_thread = threading.Thread(
target=self._video_worker, daemon=True)
self._video_thread.start()
if url.startswith('unix://'):
url = url.replace('unix://', '')
if url.startswith('/'):
self.reader, self.writer = await asyncio.open_unix_connection(url)
elif url.startswith('@'):
url = '\0' + url[1:]
self.reader, self.writer = await asyncio.open_unix_connection(url)
elif url.startswith('tcp://'):
url = url.replace('tcp://', '')
host, port = url.split(':')
self.reader, self.writer = await asyncio.open_connection(host, int(port))
else:
raise ValueError('Unsupported URL: {}'.format(url))
self.receiver = None
self.framebuffer = None
self.copytext = None
self._updating = True
self.decompressor = zlib.decompressobj()
self._input_queue = asyncio.Queue()
self._input_task = asyncio.create_task(self._input_worker())
await self._do_vnc_handshake()
return self
async def _input_worker(self):
while True:
keys, modifierkeys = await self._input_queue.get()
payload = ByteStream()
for modkey in (modifierkeys or []):
payload.add_number(4, 1) # Key event
payload.add_number(1, 1) # Down
payload.add_number(0, 2) # Padding
payload.add_number(modkey.value, 4)
payload.flush(self.writer)
await self.writer.drain()
for key in keys:
keynumber = key.value if hasattr(key, 'value') else key
payload.add_number(4, 1) # Key event
payload.add_number(1, 1) # Down
payload.add_number(0, 2) # Padding
payload.add_number(keynumber, 4)
payload.add_number(4, 1) # Key event
payload.add_number(0, 1) # Up
payload.add_number(0, 2) # Padding
payload.add_number(keynumber, 4)
payload.flush(self.writer)
await self.writer.drain()
for modkey in (modifierkeys or []):
payload.add_number(4, 1) # Key event
payload.add_number(0, 1) # Up
payload.add_number(0, 2) # Padding
payload.add_number(modkey.value, 4)
payload.flush(self.writer)
await self.writer.drain()
await asyncio.sleep(0.01) # Have to slow down keypresses for some servers
# Still shouldn't be noticable interactively, but does slow down paste to a fast typist...
self._input_queue.task_done()
async def send_keypresses(self, keys, modifierkeys=None):
await self._input_queue.put((keys, modifierkeys))
async def _read_number(self, num_bytes):
data = await self.reader.readexactly(num_bytes)
return int.from_bytes(data, byteorder='big', signed=True)
def _write_number(self, number, num_bytes):
data = number.to_bytes(num_bytes, byteorder='big', signed=True)
self.writer.write(data)
return data
async def get_screenshot(self):
while self._updating:
await asyncio.sleep(0.1)
await asyncio.sleep(0)
if self.framebuffer is None:
raise Exception('No framebuffer data available')
self._updating = True
return self.framebuffer.copy()
async def _do_vnc_handshake(self):
rfbver = await self.reader.readline()
if not rfbver.startswith(b'RFB 003.008'):
self.writer.close()
await self.writer.wait_closed()
raise Exception('Unsupported RFB version')
self.writer.write(b'RFB 003.008\n')
numsectypes = await self._read_number(1)
if not numsectypes:
self.writer.close()
await self.writer.wait_closed()
raise Exception('No security types supported by the server')
sectypes = await self.reader.readexactly(numsectypes)
sectypes = bytearray(sectypes)
secresult = 1
if 1 in sectypes:
self.writer.write(b'\x01')
await self.writer.drain()
secresult = await self._read_number(4) # Security result
if secresult != 0:
self.writer.close()
await self.writer.wait_closed()
raise Exception('VNC authentication failed')
self.writer.write(b'\x01') # Share display
self.width = await self._read_number(2)
self.height = await self._read_number(2)
pixformat = await self.reader.readexactly(16)
name_length = await self._read_number(4)
self.name = await self.reader.readexactly(name_length)
payload = ByteStream()
if pixformat != MYPIXFORMAT:
payload.add_number(0, 1) # Set pixel format
payload.add_number(0, 3) # Padding
payload.extend(MYPIXFORMAT)
payload.flush(self.writer)
self.receiver = asyncio.create_task(self._receive_loop())
payload.add_number(2, 1) # Set encodings
payload.add_number(0, 1) # Padding
payload.add_number(4, 2) # Number of encodings
payload.add_number(6, 4) # zlib
payload.add_number(7, 4) # tight
payload.add_number(-223, 4) # desktopsize
payload.add_number(-308, 4) # extended desktopsize
payload.flush(self.writer)
self._request_screen_update(incremental=False)
def _request_screen_update(self, incremental=True):
incremental = 1 if incremental else 0
payload = ByteStream()
payload.add_number(3, 1) # Framebuffer update request
payload.add_number(incremental, 1) # Incremental
payload.add_number(0, 2) # x position
payload.add_number(0, 2) # y position
payload.add_number(self.width, 2) # width
payload.add_number(self.height, 2) # height
payload.flush(self.writer)
async def _receive_loop(self):
while True:
try:
message_type = await self._read_number(1)
if message_type == 0: # Framebuffer update
await self._handle_framebuffer_update()
elif message_type == 1: # Set color map entries
raise NotImplementedError('Set color map entries not implemented')
elif message_type == 2: # Bell
pass
elif message_type == 3: # Server cut text
padding = await self._read_number(3)
length = await self._read_number(4)
self.copytext = await self.reader.readexactly(length)
else:
raise Exception(f'Unknown message type: {message_type}')
except Exception as e:
print(f"Error in receive loop: {e}")
break
async def _handle_framebuffer_update(self):
_ = await self._read_number(1) # Padding
num_rects = await self._read_number(2)
self._updating = True
for _ in range(num_rects):
await self._handle_rectangle()
self._updating = False
self._write_video_frame()
self._request_screen_update(incremental=True)
def _write_video_frame(self):
if not self._cv2 or self.framebuffer is None:
return
# Snapshot the framebuffer now and hand it to the writer thread. Frames
# captured while a write is in progress simply queue up behind it.
frame = np.ascontiguousarray(
np.array(self.framebuffer.convert('RGB'))[:, :, ::-1])
self._video_queue.put((frame, time.monotonic()))
def _video_worker(self):
cv2 = self._cv2
while True:
frame, now = self._video_queue.get()
if frame is None:
# Flush the final frame for the time it stayed on screen
if self.video_writer is not None and self._last_frame is not None:
nframes = max(1, round(
(now - self._last_frame_time) * self.fps))
for _ in range(nframes):
self.video_writer.write(self._last_frame)
if self.video_writer is not None:
self.video_writer.release()
self.video_writer = None
return
if self.video_writer is None:
self._video_size = (frame.shape[1], frame.shape[0])
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
self.video_writer = cv2.VideoWriter(
self.outputfile, fourcc, self.fps, self._video_size)
if (frame.shape[1], frame.shape[0]) != self._video_size:
frame = cv2.resize(frame, self._video_size)
if self._last_frame is None:
self._last_frame = frame
self._last_frame_time = now
continue
# Hold the previous frame for the real time it was displayed
nframes = max(1, round((now - self._last_frame_time) * self.fps))
for _ in range(nframes):
self.video_writer.write(self._last_frame)
self._last_frame = frame
self._last_frame_time = now
async def _handle_rectangle(self):
if self.framebuffer is None:
self.framebuffer = Image.new('RGBA', (self.width, self.height))
x = await self._read_number(2)
y = await self._read_number(2)
width = await self._read_number(2)
height = await self._read_number(2)
encoding_type = await self._read_number(4)
pixel_data = None
if encoding_type == 6:
compressed_data_length = await self._read_number(4)
compressed_data = await self.reader.readexactly(compressed_data_length)
# Decompress the data using zlib and store it in the framebuffer
pixel_data = self.decompressor.decompress(compressed_data)
elif encoding_type == 0:
pixel_data = await self.reader.readexactly(width * height * 4) # Assuming 32 bits per pixel
if encoding_type in (-223, -308): # desktopsize
self.width = width
self.height = height
self.framebuffer = Image.new('RGBA', (self.width, self.height))
if encoding_type == -308:
nscreens = await self._read_number(1)
_ = await self._read_number(3) # padding
for _ in range(nscreens):
_ = await self.reader.readexactly(16) # screen info
elif pixel_data:
pixel_data = np.frombuffer(pixel_data, dtype=np.uint8).reshape((height, width, 4)).copy()
pixel_data[:, :, 3] = 0xff
img = Image.fromarray(pixel_data, 'RGBA')
self.framebuffer.paste(img, (x, y))
elif encoding_type == 7: # tight
# Best document I could see was:
# https://github.com/TurboVNC/tightvnc/blob/main/vnc_winsrc/rfb/rfbproto.h
tightheader = await self._read_number(1)
streamid = tightheader & 0x0F
if streamid:
raise NotImplementedError('tight encoding with streamid not implemented')
comptype = (tightheader >> 4) & 0x0F
if comptype != 9:
raise NotImplementedError(f'tight encoding with comptype {comptype} not implemented')
compressed_data_length = await self._read_tight_length()
compressed_data = await self.reader.readexactly(compressed_data_length)
with io.BytesIO(compressed_data) as jpgimg:
img = Image.open(jpgimg)
img.load()
self.framebuffer.paste(img, (x, y))
else:
raise Exception(f'Unsupported encoding type: {encoding_type}')
async def _read_tight_length(self):
length = 0
for i in range(3):
byte = await self._read_number(1)
length |= ((byte & 0x7F) << (i * 7))
if not (byte & 0x80):
break
return length
async def close(self):
if self._video_thread is not None:
# Signal the writer thread to flush and finalize the file
self._video_queue.put((None, time.monotonic()))
await asyncio.to_thread(self._video_thread.join)
self._video_thread = None
self.writer.close()
await self.writer.wait_closed()
+1 -14
View File
@@ -11,7 +11,7 @@ Name: %{name}
Version: %{version}
Release: %{release}
Source0: %{name}-%{fversion}.tar.gz
License: Apache-2.0
License: Apache2
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
@@ -19,13 +19,6 @@ 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
@@ -48,12 +41,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
+3 -55
View File
@@ -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 importcheck 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]}))
COMPREPLY=($(compgen -W "-h -u -s -t -i" -- ${COMP_WORDS[COMP_CWORD]}))
elif [ ${CMPARGS[1]} == 'import' ] || [ ${CMPARGS[1]} == 'importcheck' ]; then
compopt -o default
COMPREPLY=()
return
elif [ ${CMPARGS[1]} == 'fetch' ]; then
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "$(osdeploy getfetchable)" -- "${COMP_WORDS[COMP_CWORD]}"))
return
fi
compopt -o dirnames
COMPREPLY=()
return
elif [ ${CMPARGS[1]} == 'updateboot' -o ${CMPARGS[1]} == 'rebase' ]; then
COMPREPLY=($(compgen -W "-n $(confetty show /deployment/profiles|sed -e 's/\///')" -- "${COMP_WORDS[COMP_CWORD]}"))
return
@@ -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
+1 -1
View File
@@ -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)
@@ -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)
+9 -57
View File
@@ -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,11 @@ 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`:
* `-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`
-41
View File
@@ -1,41 +0,0 @@
dir2img(8) -- Create a disk image from a directory for nodemedia upload
=======================================================================
## SYNOPSIS
`dir2img` [`-e <extra_mb>`] [`-s <total_mb>`] `<directory> <imagefile>` [`<label>`]
## DESCRIPTION
`dir2img` packages the contents of `<directory>` into a FAT-formatted "big
floppy" disk image written to `<imagefile>`. The resulting image is suitable
for upload with nodemedia(8) so that its files are presented to a node as
removable media. An optional volume `<label>` may be supplied.
By default the image is sized to fit the directory contents. The `-e` and `-s`
options allow reserving additional free space.
This command relies on `mtools` (specifically `mformat` and `mcopy`) being
installed.
## OPTIONS
* `-e EXTRA_MB`:
Reserve EXTRA_MB megabytes of free space in the image in addition to the
space required for the directory contents.
* `-s TOTAL_MB`:
Make the image TOTAL_MB megabytes in total, rather than sizing it to the
directory contents.
## EXAMPLES
* Create an image from a directory:
`# dir2img /var/tmp/drivers drivers.img`
* Create a 64 MB labelled image:
`# dir2img -s 64 /var/tmp/drivers drivers.img DRIVERS`
## SEE ALSO
nodemedia(8)
-19
View File
@@ -43,16 +43,6 @@
* `-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.
@@ -109,15 +99,6 @@ 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
+1 -1
View File
@@ -9,7 +9,7 @@ nodeapply(8) -- Execute command on many nodes in a noderange through ssh
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.
rerunning syncflies, and executing specified postscripts.
## OPTIONS
+3 -35
View File
@@ -26,11 +26,6 @@ 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 +33,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 +63,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 +113,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.`
@@ -20,9 +20,9 @@ nodebmcpassword(8) -- Change management controller password for a specified user
## 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`
* Reset the management controller for nodes n1 through n4:
`# nodebmcreset n1-n4`
`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)
+1 -5
View File
@@ -51,10 +51,6 @@ actually be in effect until a reboot.
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.
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
Specify a maximum number of nodes to configure, prompting if over
the threshold
@@ -72,7 +68,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
+9 -9
View File
@@ -37,7 +37,7 @@ 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.
@@ -65,9 +65,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 +102,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 +112,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`
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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`
+2 -5
View File
@@ -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`:
+4 -35
View File
@@ -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 -35
View File
@@ -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][updatestatus][update [--backup <file>]]|[<components>]`
## DESCRIPTION
@@ -20,10 +20,6 @@ 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:
@@ -40,42 +36,14 @@ the out of band facilities. Firmware updates can end in one of three states:
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
* `-p PARAMETERFILE`, `--paramaterfile=PARAMETERFILE`:
For updating, a parameter file to provife along with the update payload
* `-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)`
@@ -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 -3
View File
@@ -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`
+1 -1
View File
@@ -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,10 +1,10 @@
nodel2traceroute(8) -- Returns the layer 2 route through an Ethernet network managed by confluent given 2 end points.
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.
**nodel2traceroute** is a command that returns the layer 2 route for the configered 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.
@@ -17,7 +17,7 @@ interfaces in nodeattrib (net.*.switch) for the nodes.
## OPTIONS
* ` -e` EFACE, --eface=INTERFACE
interface to check against for the second end point or end points if checking against multiple nodes
interface to check against for the second end point or end points if using checking against multiple nodes
* ` -i` INTERFACE, --interface=INTERFACE
interface to check against for the first end point
* ` -c` CUMULUS, --cumulus=CUMULUS
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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`
+2 -2
View File
@@ -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
-34
View File
@@ -1,34 +0,0 @@
noderename(8) -- Rename nodes in the confluent management service
=================================================================
## SYNOPSIS
`noderename <noderange> <newname>`
## DESCRIPTION
`noderename` changes the name of the node or nodes matched by the given
noderange to `<newname>`. As with other attribute changes, the rename is
applied through the confluent datastore and does not by itself alter the
operating system hostname of a running node.
When renaming more than one node, `<newname>` is normally an expression so that
each matched node receives a distinct name (see noderange(5) and the attribute
expression syntax).
## OPTIONS
* `-h`, `--help`:
Show help message and exit
## EXAMPLES
* Rename a single node:
`# noderename n1 n2`
* Rename a range of nodes using an expression:
`# noderename n1-n4 'compute{n1}'`
## SEE ALSO
nodedefine(8), noderemove(8), nodeattrib(8), noderange(5)
+1 -1
View File
@@ -24,7 +24,7 @@ interval of 1 second is used.
the results, which may have a different ordering than non-CSV usage of nodesensors.
* `-i`, `--interval`=**SECONDS**:
Repeat data gathering, waiting the specified time between samples. Unless `-n` is
Repeat data gathering waiting, waiting the specified time between samples. Unless `-n` is
specified, indefinite retrieval is assumed.
* `-n`, `--numreadings`=**SAMPLES**:
+1 -1
View File
@@ -10,7 +10,7 @@ nodesetboot(8) -- Check or set next boot device for noderange
Requests that the next boot occur from the specified device. Unless otherwise
specified, this is a one time boot option, and does not change the normal boot
behavior of the system. This is useful for taking a system that normally boots
to the hard drive and starting a network install, or to go into the firmware
to the hard drive and startking a network install, or to go into the firmware
setup menu without having to hit a keystroke at the correct time on the console.
Generally, it's a bit more convenient and direct to use the nodeboot(8) command,
+2 -5
View File
@@ -30,16 +30,13 @@ as stderr, unlike psh which combines all stdout and stderr into stdout.
* `-p PORT`, `--port=PORT`
Specify a custom port for ssh
* `-s SUBSTITUTION`, `--substitutename=SUBSTITUTION`
* `-s SUBSTITUTION`, `--substitutename=SUBSTITITUTION`
Specify a substitution name instead of the nodename. If no {} are in the substitution,
it is considered to be an append. For example, '-s -ib' would produce 'node1-ib' from 'node1'.
Full expression syntax is supported, in which case the substitution is considered to be the entire
new name. {node}-ib would be equivalent to -ib. For example, nodeshell -s {bmc} node1
new name. {node}-ib would be equivalent to -ib. For example, nodeshell -s {bmc} node1
would ssh to the BMC instead of the node.
* `-t TIMEOUT`, `--timeout=TIMEOUT`
Timeout in seconds for each node ssh connection attempt
## EXAMPLES
* Running `echo hi` on for nodes:
+3 -3
View File
@@ -3,7 +3,7 @@ stats(8) -- Common basic statistics on typical numeric data in output
## SYNOPSIS
`<other command> | stats [-c N] [-d D] [-x|-g|-t|-o image.png] [-s N] [-v] [-b N]`
`<other command> | stats [-c N] [-d D] [-x|-g|-t|-o image.png] [-s N] [-v] [-b N]
## DESCRIPTION
@@ -11,11 +11,11 @@ The **stats** command helps analyze common numerical data such as performance nu
or temperatures or any other numerical value.
By default it looks for the last numerical output on the first line to identify the numerical column
and analyze that number. This can be overridden by **-c COLUMN** to indicate a column. By default,
and analyze that number. This can be overriden by **-c COLUMN** to indicate a column. By default,
whitespace and commas are treated to delimit columns, and **-d DELIMITER** can be used to override.
By default it outputs basic statistics, but a histogram is available either text or through X11 output
or sixel or output to an image file depending on whether **-x**, **-g**, **-t**, or **-o image.png** is
or sixel or output to an image file depending on whether **-x**, **-g**, **-t*, or **-o image.png** is
used.
## OPTIONS
+1 -14
View File
@@ -5,20 +5,7 @@ cd `dirname $0`/doc/man
mkdir -p ../../man/man1
mkdir -p ../../man/man5
mkdir -p ../../man/man8
for ronn in *.ronn; do
# The first heading line is of the form "name(section) -- description"
header=`grep -m1 -E '\([0-9]+\) *--+ ' "$ronn"`
name=`echo "$header" | sed -E 's/^#* *([A-Za-z0-9._-]+)\(([0-9]+)\).*/\1/'`
section=`echo "$header" | sed -E 's/^#* *([A-Za-z0-9._-]+)\(([0-9]+)\).*/\2/'`
# Strip the ronn title heading (and any setext underline) and promote the
# remaining section headings so pandoc emits them as man .SH sections.
sed -E '/\([0-9]+\) *--+ /d; /^=+$/d' "$ronn" | \
pandoc --standalone --from markdown --to man \
--shift-heading-level-by=-1 \
--metadata title="$name" \
--metadata section="$section" \
-o "$name.$section"
done
ronn -r *.ronn
mv *.1 ../../man/man1/
mv *.5 ../../man/man5/
mv *.8 ../../man/man8/
@@ -13,7 +13,6 @@
import confluent.client as cl
import socket
import struct
import sys
c = cl.Command()
macs = []
interface = sys.argv[1]
+1
View File
@@ -0,0 +1 @@
1.0.1
+11 -4
View File
@@ -1,14 +1,21 @@
cd $(dirname $0)
VERSION=`../mkversion --tilde` || exit 1
VERSION=`git describe|cut -d- -f 1`
NUMCOMMITS=`git describe|cut -d- -f 2`
if [ "$NUMCOMMITS" != "$VERSION" ]; then
LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev)
LASTNUM=$((LASTNUM+1))
FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev)
VERSION=${FIRSTPART}.${LASTNUM}
VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3`
fi
sed -e "s/#VERSION#/$VERSION/" confluent_osdeploy.spec.tmpl > confluent_osdeploy.spec
cp ../LICENSE .
cd ..
cp ../LICENSE .
tar Jcvf confluent_osdeploy.tar.xz confluent_osdeploy
mv confluent_osdeploy.tar.xz ~/rpmbuild/SOURCES/
cd -
mkdir -p el9bin/opt/confluent/bin
mkdir -p el9bin/stateless-bin
podman run --privileged --rm -v $(pwd)/utils:/buildutils -i -t el9build make -C /buildutils
podman run --privileged --rm -v $(pwd)/utils:/buildutils -i -t fedorabuild make -C /buildutils
cd utils
cp confluent_imginfo copernicus clortho autocons ../el9bin/opt/confluent/bin
cp start_root urlmount ../el9bin/stateless-bin/
+10 -2
View File
@@ -1,8 +1,16 @@
cd $(dirname $0)
VERSION=`../mkversion --tilde` || exit 1
VERSION=`git describe|cut -d- -f 1`
NUMCOMMITS=`git describe|cut -d- -f 2`
if [ "$NUMCOMMITS" != "$VERSION" ]; then
LASTNUM=$(echo $VERSION|rev|cut -d . -f 1|rev)
LASTNUM=$((LASTNUM+1))
FIRSTPART=$(echo $VERSION|rev|cut -d . -f 2- |rev)
VERSION=${FIRSTPART}.${LASTNUM}
VERSION=$VERSION~dev$NUMCOMMITS+`git describe|cut -d- -f 3`
fi
sed -e "s/#VERSION#/$VERSION/" confluent_osdeploy-aarch64.spec.tmpl > confluent_osdeploy-aarch64.spec
cp ../LICENSE .
cd ..
cp ../LICENSE .
tar Jcvf confluent_osdeploy.tar.xz confluent_osdeploy
mv confluent_osdeploy.tar.xz ~/rpmbuild/SOURCES/
cd -
@@ -6,12 +6,14 @@ except ImportError:
import base64
import ctypes
import ctypes.util
import glob
import os
import select
import socket
import subprocess
import ssl
import sys
import struct
import time
import re
import hashlib
@@ -22,10 +24,6 @@ except ImportError:
json = None
hmac = None
# Ignore proxy-based environment variables
for proxy_var in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy']:
os.environ.pop(proxy_var, None)
class InvalidApiKey(Exception):
pass
@@ -161,26 +159,12 @@ def scan_confluents(confuuid=None):
srvs = {}
s6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
try:
s6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except Exception:
# REUSPORT is ideal, but if python is missing it, just live with the limitation
pass
try: # We would like to bind for firewall rules, however if not possible then leave it alone
s6.bind(('::', 1900))
except Exception:
pass
s6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s6.bind(('::', 1900))
s4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
try:
s4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except Exception:
# REUSPORT is ideal, but if python is missing it, just live with the limitation
pass
try:
s4.bind(('0.0.0.0', 1900))
except Exception:
pass
s4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s4.bind(('0.0.0.0', 1900))
doneidxs = set([])
msg = 'M-SEARCH * HTTP/1.1\r\nST: urn:xcat.org:service:confluent:'
if not confuuid and os.path.exists('/etc/confluent/confluent.deploycfg'):
@@ -36,36 +36,6 @@ def add_lla(iface, mac):
return None
return lla
def create_systemd_link(nic, altnames):
if not os.path.exists('/etc/systemd/network'):
return
for disallowed in ['..', ' ', '/', '\n', '=']:
if disallowed in nic:
raise ValueError('Invalid nic name')
if not isinstance(altnames, list):
altnames = altnames.split(',')
filteredaltnames = []
for altname in altnames:
altname = altname.strip()
for disallowed in ['..', ' ', '/', '\n', '=']:
if disallowed in altname:
break
else:
if altname and altname not in filteredaltnames:
filteredaltnames.append(altname)
with open(f'/sys/class/net/{nic}/address') as macin:
macaddr = macin.read().strip()
with open(f'/etc/systemd/network/00-{nic}.link', 'w') as linkout:
linkout.write('[Match]\n')
linkout.write(f'MACAddress={macaddr}\n\n')
linkout.write('[Link]\n')
linkout.write('NamePolicy=kernel database onboard slot path\n')
linkout.write('AlternativeNamesPolicy=database onboard slot path mac\n')
for altname in filteredaltnames:
linkout.write(f'AlternativeName={altname}\n')
subprocess.check_call(['udevadm', 'trigger', '--action=add', '--subsystem-match=net'])
#cli = apiclient.HTTPSClient(json=True)
#c = cli.grab_url_with_status('/confluent-api/self/netcfg')
def add_missing_llas():
@@ -144,14 +114,6 @@ def get_interface_name(iname, settings):
return iname
return None
def parse_extra_settings(stgs):
extras = {}
for kv in stgs.get('extra_settings', '').split(';'):
k, _, v = kv.partition('=')
if k.strip():
extras[k.strip()] = v.strip()
return extras
class NetplanManager(object):
def __init__(self, deploycfg):
self.cfgbydev = {}
@@ -189,9 +151,8 @@ class NetplanManager(object):
currinfo[devname]['routes'] = routeinfo
currcfg[devname] = currinfo[devname]
def apply_configuration(self, cfg, lastchance=False):
def apply_configuration(self, cfg):
devnames = cfg['interfaces']
teammode = None
if len(devnames) > 1:
teammode = cfg['settings'].get('team_mode', None)
if not teammode:
@@ -268,29 +229,8 @@ class NetplanManager(object):
if dnsdomain not in currdnsdomain:
needcfgwrite = True
currdnsdomain.append(dnsdomain)
extras = parse_extra_settings(stgs)
if extras:
currcfg = self.cfgbybond if devname in self.cfgbybond else self.cfgbydev
devdict = currcfg.setdefault(devname, {})
for key in extras:
try:
val = yaml.safe_load(extras[key])
except yaml.YAMLError:
val = extras[key]
keyptr = devdict
keypath = key.split('.')
for k in keypath[:-1]:
if not isinstance(keyptr.get(k, None), dict):
keyptr[k] = {}
keyptr = keyptr[k]
if keyptr.get(keypath[-1], None) != val:
needcfgwrite = True
keyptr[keypath[-1]] = val
prune_from_cloudinit = []
if needcfgwrite:
connname = cfg['settings'].get('connection_name', None)
if not teammode and connname and connname != devname:
create_systemd_link(devname, [connname])
needcfgapply = True
oumask = os.umask(0o77)
if devname in self.cfgbydev:
@@ -303,11 +243,9 @@ class NetplanManager(object):
prune_from_cloudinit.append(iface)
with open('/etc/netplan/10-{0}-confluentcfg.yaml'.format(iface), 'w') as planout:
planout.write(yaml.dump({'network': {'version': 2, 'ethernets': {iface: {'dhcp4': False}}}}))
os.chmod('/etc/netplan/10-{0}-confluentcfg.yaml'.format(iface), 0o600)
cfgfile = '/etc/netplan/30-{0}-confluentcfg.yaml'.format(devname)
with open(cfgfile, 'w') as planout:
planout.write(yaml.dump(newcfg))
os.chmod(cfgfile, 0o600)
os.umask(oumask)
if prune_from_cloudinit:
prunecfgs = ['/etc/netplan/00-installer-config.yaml',
@@ -319,7 +257,7 @@ class NetplanManager(object):
continue
with open(defcfg, 'r') as cloudinit:
cloudinfo = yaml.safe_load(cloudinit)
if not cloudinfo or 'network' not in cloudinfo:
if 'network' not in cloudinfo:
continue
for clouddev in list(cloudinfo.get('network', {}).get('ethernets', {})):
if clouddev in prune_from_cloudinit:
@@ -378,7 +316,7 @@ class WickedManager(object):
v = v.strip()
currcfg[k] = v
def apply_configuration(self, cfg, lastchance=False):
def apply_configuration(self, cfg):
stgs = cfg['settings']
ipcfg = 'STARTMODE=auto\n'
routecfg = ''
@@ -398,9 +336,6 @@ class WickedManager(object):
if stgs.get('ipv6_address', None):
ipcfg += 'IPADDR_V6=' + stgs['ipv6_address'] + '\n'
v6gw = stgs.get('ipv6_gateway', None)
extras = parse_extra_settings(stgs)
for key in extras:
ipcfg += '{0}={1}\n'.format(key, shlex.quote(extras[key]))
cname = None
if len(cfg['interfaces']) > 1: # creating new team
if not stgs.get('team_mode', None):
@@ -540,7 +475,6 @@ class NetworkManager(object):
cmdargs['ipv4.dns'] = ','.join(dns4)
if dns6:
cmdargs['ipv6.dns'] = ','.join(dns6)
cmdargs.update(parse_extra_settings(stgs))
if len(cfg['interfaces']) > 1: # team time.. should be..
if not cfg['settings'].get('team_mode', None):
sys.stderr.write("Warning, multiple interfaces ({0}) without a team_mode, skipping setup\n".format(','.join(cfg['interfaces'])))
@@ -584,8 +518,6 @@ class NetworkManager(object):
return
cname = iname if not cname else cname
u = self.uuidbyname.get(cname, None)
if cname != iname:
create_systemd_link(iname, [cname])
cargs = []
for arg in cmdargs:
cargs.append(arg)
@@ -663,18 +595,18 @@ if __name__ == '__main__':
time.sleep(1)
continue
dc = json.loads(dc)
inames = get_interface_name(idxmap[curridx], nc.get('default', {}))
if inames:
for iname in inames.split(','):
iname = get_interface_name(idxmap[curridx], nc.get('default', {}))
if iname:
for iname in iname.split(','):
if 'default' in netname_to_interfaces:
netname_to_interfaces['default']['interfaces'].add(iname)
else:
netname_to_interfaces['default'] = {'interfaces': set([iname]), 'settings': nc['default']}
for netname in nc.get('extranets', {}):
uname = '_' + netname
inames = get_interface_name(idxmap[curridx], nc['extranets'][netname])
if inames:
for iname in inames.split(','):
iname = get_interface_name(idxmap[curridx], nc['extranets'][netname])
if iname:
for iname in iname.split(','):
if uname in netname_to_interfaces:
netname_to_interfaces[uname]['interfaces'].add(iname)
else:
@@ -1,310 +0,0 @@
#!/usr/bin/python3
import json
import subprocess
import os
import shutil
class SilentException(Exception):
pass
# List of models that are generally used for OS install disk (generally 2-disk mirroring)
PRIORITY_MODELS = ('m.2 nvme 2-bay raid kit',
'thinksystem_m.2_vd',
'thinksystem m.2',
'thinksystem_m.2',
'thinksystem 7mm',
'thinksystem_7mm',
'thinksystem_7mm_vd',
'b540p-2hs', # 7mm or M.2 in rear
'b540d-2hs', # M.2 in E3.S or 2.5inch
'b550d-2hs', # Front M.2
'b550p-2hs', # Rear M.2
'raid b540p-2hs',
'raid b540d-2hs',
'raid b550d-2hs',
'raid b550p-2hs',
'raid b540i-2i',
'raid_b540i-2i',
'raid b545i-2i',
'raid b545i-2i',
'raid_b545-2i')
BOOT_SLOT_KEYWORDS = ('m.2 socket', '7mm')
class DiskInfo(object):
def __init__(self, devname, slotinfo=None):
if devname.startswith('nvme') and 'c' in devname:
raise Exception("Skipping multipath devname")
self.name = devname
self.wwn = None
self.path = None
self.model = ''
self.size = 0
self.driver = ''
self.mdcontainer = ''
self.subsystype = ''
self.kernnames = []
self.vrocmembers = []
self.is_m2 = False
self.in_vroc = False
devnode = '/dev/{0}'.format(devname)
qprop = subprocess.check_output(
['udevadm', 'info', '--query=property', devnode])
if not isinstance(qprop, str):
qprop = qprop.decode('utf8')
for prop in qprop.split('\n'):
if '=' not in prop:
continue
k, v = prop.split('=', 1)
if k == 'DEVTYPE' and v != 'disk':
if v == 'partition':
raise SilentException('Partition')
raise Exception('Not a disk')
elif k == 'DM_NAME':
raise SilentException('Device Mapper')
elif k == 'ID_MODEL':
self.model = v
elif k == 'DEVPATH':
self.path = v
elif k == 'ID_WWN':
self.wwn = v
elif k == 'MD_CONTAINER':
self.mdcontainer = v
attrs = subprocess.check_output(['udevadm', 'info', '-a', devnode])
if not isinstance(attrs, str):
attrs = attrs.decode('utf8')
for attr in attrs.split('\n'):
if '==' not in attr:
continue
k, v = attr.split('==', 1)
k = k.strip()
if k == 'ATTRS{size}':
self.size = v.replace('"', '')
elif (k == 'DRIVERS' and not self.driver
and v not in ('"sd"', '""')):
self.driver = v.replace('"', '')
elif k == 'ATTRS{subsystype}':
self.subsystype = v.replace('"', '')
elif k == 'ATTR{ro}' and v == '"1"':
raise Exception("Device is read-only")
elif k == 'ATTRS{removable}' and v == '"1"':
raise Exception("Device is removable")
elif k == 'ATTRS{address}':
self.busaddr = v.replace('"', '')
elif k == 'KERNELS':
self.kernnames.append(v.replace('"', ''))
if not getattr(self, 'busaddr', None):
for kernname in getattr(self, 'kernnames', []):
if kernname in slotinfo:
self.is_m2 = True
break
if getattr(self, 'busaddr', None) and self.busaddr in slotinfo:
self.is_m2 = True
if not self.driver and 'imsm' not in self.mdcontainer and self.subsystype != 'nvm':
raise Exception("No driver detected")
if 'imsm' in self.mdcontainer:
try:
mdinfo = subprocess.check_output(
['mdadm', '--detail', '-Y', devnode],
stderr=subprocess.DEVNULL)
if not isinstance(mdinfo, str):
mdinfo = mdinfo.decode('utf8', errors='ignore')
for line in mdinfo.splitlines():
key, separator, value = line.partition('=')
if separator and key.endswith('_DEV'):
member = value.strip()
member = member.replace('/dev/', '')
if member and member not in self.vrocmembers:
self.vrocmembers.append(member)
except Exception:
pass
if self.driver == 'sr':
raise Exception('cd/dvd')
if os.path.exists('/sys/block/{0}/size'.format(self.name)):
with open('/sys/block/{0}/size'.format(self.name), 'r') as sizesrc:
self.size = int(sizesrc.read()) * 512
if int(self.size) < 2147483648:
raise Exception("Device too small for install ({}MiB)".format(int(self.size)/1024/1024))
@property
def priority(self):
if self.in_vroc:
return 99 # prefer the assembled array over the member disks
if self.is_m2:
return 0
if self.model.lower() in PRIORITY_MODELS:
return 0
if 'imsm' in self.mdcontainer:
return 1
if self.driver == 'ahci':
return 2
if self.driver.startswith('megaraid'):
return 3
if self.driver.startswith('mpt'):
return 4
return 99
def __repr__(self):
return repr({
'name': self.name,
'path': self.path,
'wwn': self.wwn,
'driver': self.driver,
'size': self.size,
'model': self.model,
})
def get_dmi_type9_info():
dmidecode = shutil.which('dmidecode')
if not dmidecode:
return {}
try:
dmio = subprocess.check_output(
[dmidecode, '--type', '9'], stderr=subprocess.DEVNULL)
except Exception:
return {}
if not isinstance(dmio, str):
dmio = dmio.decode('utf8', errors='ignore')
slots = {}
currentslot = None
for line in dmio.split('\n'):
stripped = line.strip()
if not stripped:
if currentslot:
busaddr = currentslot.get('bus_address', None)
if busaddr:
slots[busaddr] = currentslot
currentslot = None
continue
if stripped.startswith('Handle ') and ', DMI type 9,' in stripped:
if currentslot:
busaddr = currentslot.get('bus_address', None)
if busaddr:
slots[busaddr] = currentslot
currentslot = {'handle': stripped.split(',', 1)[0].split(' ', 1)[1]}
continue
if currentslot is None:
continue
if ':' not in stripped:
continue
key, val = stripped.split(':', 1)
key = key.strip().lower().replace(' ', '_')
currentslot[key] = val.strip()
if currentslot:
busaddr = currentslot.get('bus_address', None)
if busaddr:
slots[busaddr] = currentslot
return slots
def get_m2_slot_info():
slots = get_dmi_type9_info()
m2slots = {}
for slot in slots.values():
label = '{0} {1}'.format(
slot.get('type', ''), slot.get('designation', '')).lower()
if not any(x in label for x in BOOT_SLOT_KEYWORDS):
continue
busaddr = slot.get('bus_address', None)
if not busaddr:
continue
m2slots[busaddr] = slot
return m2slots
def get_deployment_storage():
apiclient = None
for clientpath in ('/opt/confluent/bin/apiclient', '/etc/confluent/apiclient'):
if os.path.exists(clientpath):
apiclient = clientpath
break
if not apiclient:
return None
for attriburl in ('/confluent-api/self/myattrib', '/confluent-api/self/myattribs'):
try:
attribs = subprocess.check_output(
['python3', apiclient, attriburl, '-j'], stderr=subprocess.DEVNULL)
if not isinstance(attribs, str):
attribs = attribs.decode('utf8')
attribs = json.loads(attribs)
if not isinstance(attribs, dict):
continue
storage = attribs.get('deployment.storage', None)
if isinstance(storage, str) and storage.strip():
return storage.strip()
except Exception:
continue
return None
def filter_deployment_storage(disks, storagespec, slotinfo=None):
if not storagespec:
return disks
spec = storagespec.strip().lower()
for disk in list(disks):
if spec == 'm2':
if disk.is_m2:
continue
if disk.model.lower() in PRIORITY_MODELS:
continue
if slotinfo is None:
slotinfo = get_m2_slot_info()
if not getattr(disk, 'busaddr', None):
for kernname in getattr(disk, 'kernnames', []):
if kernname in slotinfo:
break
else:
disks.remove(disk)
continue
if disk.busaddr not in slotinfo:
disks.remove(disk)
elif spec.startswith('/dev/'):
bspec = os.path.basename(spec)
if disk.name.lower() == bspec:
return [disk]
else:
raise Exception("Unknown deployment.storage specification: {0}".format(storagespec))
if not disks:
raise Exception("No disks match deployment.storage specification: {0}".format(storagespec))
return disks
def attach_member_disks(disks):
for disk in disks:
num_vroc_members = len(getattr(disk, 'vrocmembers', []))
for vrocmember in getattr(disk, 'vrocmembers', []):
for memberdisk in disks:
if memberdisk.name == vrocmember:
memberdisk.in_vroc = True
if memberdisk.is_m2:
disk.is_m2 = True
else:
if num_vroc_members == 2 and 'nvme' in vrocmember:
# This is not assured, but is likely
# Unfortunately, some VMD configurations blow past any way to detect form factor
# as DMI table may not include
disk.is_m2 = True
def main():
disks = []
slotinfo = get_m2_slot_info()
for disk in sorted(os.listdir('/sys/class/block')):
try:
disk = DiskInfo(disk, slotinfo=slotinfo)
disks.append(disk)
except SilentException:
pass
except Exception as e:
print("Skipping {0}: {1}".format(disk, str(e)))
attach_member_disks(disks)
disks = filter_deployment_storage(disks, get_deployment_storage(), slotinfo=slotinfo)
nd = [x.name for x in sorted(disks, key=lambda x: [x.priority, x.size])]
if nd:
open('/tmp/installdisk', 'w').write(nd[0])
else:
raise Exception("No suitable install disk found")
if __name__ == '__main__':
main()
@@ -2,8 +2,6 @@
[ -f /etc/confluent/functions ] && . /etc/confluent/functions
[ -f /opt/confluent/bin/apiclient ] && confapiclient=/opt/confluent/bin/apiclient
[ -f /etc/confluent/apiclient ] && confapiclient=/etc/confluent/apiclient
unset http_proxy
unset https_proxy
for pubkey in /etc/ssh/ssh_host*key.pub; do
if [ "$pubkey" = /etc/ssh/ssh_host_key.pub ]; then
continue
@@ -1,123 +0,0 @@
#!/bin/bash
# Resealing to allow more practical use of, for example, PCR1, which frequently gets changed booting from install environment
# to local disk
get_requested_pcrs() {
local encryptboot=$(grep ^encryptboot: /etc/confluent/confluent.deploycfg | awk '{print $2}')
IFS=':' read -ra params <<< "$encryptboot"
for param in "${params[@]}"; do
if [[ "$param" =~ ^pcrs= ]]; then
echo "${param#pcrs=}"
return 0
fi
done
}
wipe_slots() {
local device="$1"
if [[ -z "$device" ]]; then
echo "Error: No device specified" >&2
return 1
fi
# Check if device exists
if [[ ! -e "$device" ]]; then
echo "Error: Device $device not found" >&2
return 1
fi
# Wipe existing TPM2 slots using clevis
echo "Wiping TPM2 slots for $device"
if [ -e /bin/systemd-cryptenroll ]; then
systemd-cryptenroll --wipe-slot=tpm2 "$device"
else
clevis luks unbind -d "$device" tpm2 || true
fi
return 0
}
seal_tpm() {
local device="$1"
if [[ -z "$device" ]]; then
echo "Error: No device specified" >&2
return 1
fi
if [[ ! -e "$device" ]]; then
echo "Error: Device $device not found" >&2
return 1
fi
local pcrs=$(get_requested_pcrs)
if [ -e /bin/systemd-cryptenroll ]; then
PASSWORD=$(cat /etc/confluent/luks.key) systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs="$pcrs" "$device"
else
if [ -n "$pcrs" ]; then
clevispcrs=$(echo "$pcrs" | sed -e 's/,/, /g')
clevis luks bind -f -d "$device" -k - tpm2 "{\"pcr_ids\":\"$clevispcrs\"}" < /etc/confluent/luks.key
else
clevis luks bind -f -d "$device" -k - tpm2 "{}" < /etc/confluent/luks.key
fi
fi
if [[ $? -eq 0 ]]; then
echo "Successfully enrolled TPM2 for $device"
return 0
else
echo "Failed to enroll TPM2 for $device" >&2
return 1
fi
}
reseal_luks() {
local device="$1"
if [[ -z "$device" ]]; then
echo "Error: No device specified" >&2
return 1
fi
# Check if device exists
if [[ ! -e "$device" ]]; then
echo "Error: Device $device not found" >&2
return 1
fi
# Wipe existing TPM2 slots
wipe_slots "$device"
# Enroll TPM2 key
seal_tpm "$device"
}
while IFS= read -r line; do
# Skip comments and empty lines
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
device=$(echo "$line" | awk '{print $2}')
# If it's a UUID, convert to device name
if [[ "$device" =~ ^UUID= ]]; then
device=$(blkid -U "${device#UUID=}")
fi
if [ -e /bin/systemd-cryptenroll ]; then
systemd-cryptenroll "$device" | grep -q "tpm2" || continue
else
clevis luks list -d "$device" | grep -q "tpm2" || continue
fi
# Process the device
if [[ -n "$device" ]]; then
reseal_luks "$device"
else
echo "Warning: Could not determine device for line: $line" >&2
fi
done < /etc/crypttab
@@ -3,13 +3,13 @@ Version: #VERSION#
Release: 1
Summary: OS Deployment support for confluent
License: Apache-2.0
License: Apache2
URL: https://hpc.lenovo.com/
Source0: confluent_osdeploy.tar.xz
Source1: confluent_el9bin.tar.xz
Source2: confluent_el8bin.tar.xz
BuildArch: noarch
Requires: confluent_ipxe-aarch64 mtools tar
Requires: confluent_ipxe mtools tar
BuildRoot: /tmp
%description
@@ -31,7 +31,7 @@ cp -a debian debian13
mkdir -p debian13/initramfs/usr
mv debian13/initramfs/lib debian13/initramfs/usr/
mv el10/initramfs/usr el10/initramfs/var
for os in rhvh4 el7 genesis el8 suse15 suse16 debian debian13 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
mkdir ${os}out
cd ${os}out
if [ -d ../${os}bin ]; then
@@ -41,13 +41,13 @@ for os in rhvh4 el7 genesis el8 suse15 suse16 debian debian13 ubuntu20.04 ubuntu
else
cp -a ../opt .
fi
cp -a ../common/initramfs/* .
cp -a ../${os}/initramfs/* .
cp -a ../common/initramfs/* .
find . | cpio -H newc -o > ../addons.cpio
mv ../addons.cpio .
cd ..
done
for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
mkdir ${os}disklessout
cd ${os}disklessout
if [ -d ../${os}bin ]; then
@@ -57,8 +57,8 @@ for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubu
else
cp -a ../opt .
fi
cp -a ../common/initramfs/* .
cp -a ../${os}-diskless/initramfs/* .
cp -a ../common/initramfs/* .
if [ -d ../${os}bin ]; then
cp -a ../${os}bin/stateless-bin/* opt/confluent/bin
else
@@ -71,8 +71,8 @@ done
mkdir esxi7out
cd esxi7out
cp -a ../opt .
cp -a ../common/initramfs/* .
cp -a ../esxi7/initramfs/* .
cp -a ../common/initramfs/* .
chmod +x bin/* opt/confluent/bin/*
tar zcvf ../addons.tgz *
mv ../addons.tgz .
@@ -88,7 +88,7 @@ mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/common
cp common/initramfs/opt/confluent/bin/apiclient %{buildroot}/opt/confluent/lib/osdeploy/common
for os in rhvh4 el7 el8 el9 el10 debian debian13 genesis suse15 suse16 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 coreos; do
for os in rhvh4 el7 el8 el9 el10 debian debian13 genesis suse15 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 coreos; do
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs/aarch64/
cp ${os}out/addons.* %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs/aarch64/
if [ -d ${os}disklessout ]; then
@@ -3,7 +3,7 @@ Version: #VERSION#
Release: 1
Summary: OS Deployment support for confluent
License: Apache-2.0
License: Apache2
URL: https://hpc.lenovo.com/
Source0: confluent_osdeploy.tar.xz
Source1: confluent_el9bin.tar.xz
@@ -33,7 +33,7 @@ cp -a debian debian13
mkdir -p debian13/initramfs/usr
mv debian13/initramfs/lib debian13/initramfs/usr/
mv el10/initramfs/usr el10/initramfs/var
for os in rhvh4 el7 genesis el8 suse15 suse16 debian debian13 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
mkdir ${os}out
cd ${os}out
if [ -d ../${os}bin ]; then
@@ -43,13 +43,13 @@ for os in rhvh4 el7 genesis el8 suse15 suse16 debian debian13 ubuntu18.04 ubuntu
else
cp -a ../el8bin/opt .
fi
cp -a ../common/initramfs/* .
cp -a ../${os}/initramfs/* .
cp -a ../common/initramfs/* .
find . | cpio -H newc -o > ../addons.cpio
mv ../addons.cpio .
cd ..
done
for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
mkdir ${os}disklessout
cd ${os}disklessout
if [ -d ../${os}bin ]; then
@@ -59,8 +59,8 @@ for os in el7 el8 suse15 suse16 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubu
else
cp -a ../el8bin/opt .
fi
cp -a ../common/initramfs/* .
cp -a ../${os}-diskless/initramfs/* .
cp -a ../common/initramfs/* .
if [ -d ../${os}bin ]; then
cp -a ../${os}bin/stateless-bin/* opt/confluent/bin
else
@@ -73,8 +73,8 @@ done
mkdir esxi7out
cd esxi7out
cp -a ../el8bin/opt .
cp -a ../common/initramfs/* .
cp -a ../esxi7/initramfs/* .
cp -a ../common/initramfs/* .
chmod +x bin/* opt/confluent/bin/*
tar zcvf ../addons.tgz *
mv ../addons.tgz .
@@ -89,7 +89,7 @@ cp -a esxi7 esxi9
%install
mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
cp LICENSE %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
for os in rhvh4 el7 el8 el9 el10 genesis suse15 suse16 ubuntu20.04 debian debian13 ubuntu18.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 esxi9 coreos; do
for os in rhvh4 el7 el8 el9 el10 genesis suse15 ubuntu20.04 debian debian13 ubuntu18.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 esxi9 coreos; do
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles
cp ${os}out/addons.* %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs
@@ -97,7 +97,6 @@ for os in rhvh4 el7 el8 el9 el10 genesis suse15 suse16 ubuntu20.04 debian debian
for targ in %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles/*; do
cp -a common/profile/* $targ
done
cp -a $os/profiles/* %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles
if [ -d ${os}disklessout ]; then
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/${os}-diskless/initramfs
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/${os}-diskless/profiles
@@ -106,7 +105,6 @@ for os in rhvh4 el7 el8 el9 el10 genesis suse15 suse16 ubuntu20.04 debian debian
for targ in %{buildroot}/opt/confluent/lib/osdeploy/$os-diskless/profiles/*; do
cp -a common/profile/* $targ
done
cp -a ${os}-diskless/profiles/* %{buildroot}/opt/confluent/lib/osdeploy/$os-diskless/profiles
fi
done
find %{buildroot}/opt/confluent/lib/osdeploy/ -name .gitignore -exec rm -f {} +
@@ -111,14 +111,6 @@ def get_interface_name(iname, settings):
return iname
return None
def parse_extra_settings(stgs):
extras = {}
for kv in stgs.get('extra_settings', '').split(';'):
k, _, v = kv.partition('=')
if k.strip():
extras[k.strip()] = v.strip()
return extras
class NetplanManager(object):
def __init__(self, deploycfg):
self.cfgbydev = {}
@@ -151,7 +143,7 @@ class NetplanManager(object):
nicinfo[devname]['routes'] = routeinfo
self.cfgbydev[devname] = nicinfo[devname]
def apply_configuration(self, cfg, lastchance=False):
def apply_configuration(self, cfg):
devnames = cfg['interfaces']
if len(devnames) != 1:
raise Exception('Multi-nic team/bonds not yet supported')
@@ -200,23 +192,6 @@ class NetplanManager(object):
if dnsdomain not in currdnsdomain:
needcfgwrite = True
currdnsdomain.append(dnsdomain)
extras = parse_extra_settings(stgs)
if extras:
devdict = self.cfgbydev.setdefault(devname, {})
for key in extras:
try:
val = yaml.safe_load(extras[key])
except yaml.YAMLError:
val = extras[key]
keyptr = devdict
keypath = key.split('.')
for k in keypath[:-1]:
if not isinstance(keyptr.get(k, None), dict):
keyptr[k] = {}
keyptr = keyptr[k]
if keyptr.get(keypath[-1], None) != val:
needcfgwrite = True
keyptr[keypath[-1]] = val
if needcfgwrite:
needcfgapply = True
newcfg = {'network': {'version': 2, 'ethernets': {devname: self.cfgbydev[devname]}}}
@@ -266,7 +241,7 @@ class WickedManager(object):
v = v.strip()
currcfg[k] = v
def apply_configuration(self, cfg, lastchance=False):
def apply_configuration(self, cfg):
stgs = cfg['settings']
ipcfg = 'STARTMODE=auto\n'
routecfg = ''
@@ -286,9 +261,6 @@ class WickedManager(object):
if stgs.get('ipv6_address', None):
ipcfg += 'IPADDR_V6=' + stgs['ipv6_address'] + '\n'
v6gw = stgs.get('ipv6_gateway', None)
extras = parse_extra_settings(stgs)
for key in extras:
ipcfg += '{0}={1}\n'.format(key, shlex.quote(extras[key]))
cname = None
if len(cfg['interfaces']) > 1: # creating new team
if not stgs.get('team_mode', None):
@@ -428,7 +400,6 @@ class NetworkManager(object):
cmdargs['ipv4.dns'] = ','.join(dns4)
if dns6:
cmdargs['ipv6.dns'] = ','.join(dns6)
cmdargs.update(parse_extra_settings(stgs))
if len(cfg['interfaces']) > 1: # team time.. should be..
if not cfg['settings'].get('team_mode', None):
sys.stderr.write("Warning, multiple interfaces ({0}) without a team_mode, skipping setup\n".format(','.join(cfg['interfaces'])))
@@ -545,18 +516,18 @@ if __name__ == '__main__':
time.sleep(1)
continue
dc = json.loads(dc)
inames = get_interface_name(idxmap[curridx], nc.get('default', {}))
if inames:
for iname in inames.split(','):
iname = get_interface_name(idxmap[curridx], nc.get('default', {}))
if iname:
for iname in iname.split(','):
if 'default' in netname_to_interfaces:
netname_to_interfaces['default']['interfaces'].add(iname)
else:
netname_to_interfaces['default'] = {'interfaces': set([iname]), 'settings': nc['default']}
for netname in nc.get('extranets', {}):
uname = '_' + netname
inames = get_interface_name(idxmap[curridx], nc['extranets'][netname])
if inames:
for iname in inames.split(','):
iname = get_interface_name(idxmap[curridx], nc['extranets'][netname])
if iname:
for iname in iname.split(','):
if uname in netname_to_interfaces:
netname_to_interfaces[uname]['interfaces'].add(iname)
else:
@@ -4,7 +4,7 @@ function test_mgr() {
if [[ "$whost" == *:* ]] && [[ "$whost" != *[* ]] ; then
whost="[$whost]"
fi
if curl -gs -x "" https://${whost}/confluent-api/ > /dev/null; then
if curl -gs https://${whost}/confluent-api/ > /dev/null; then
return 0
fi
return 1
@@ -64,9 +64,9 @@ function set_confluent_vars() {
}
fetch_remote() {
curlargs=(-x "")
curlargs=""
if [ -f /etc/confluent/ca.pem ]; then
curlargs=(-x "" --cacert /etc/confluent/ca.pem)
curlargs=" --cacert /etc/confluent/ca.pem"
fi
set_confluent_vars
mkdir -p $(dirname $1)
@@ -74,7 +74,7 @@ fetch_remote() {
if [[ "$whost" == *:* ]] && [[ "$whost" != *[* ]] ; then
whost="[$whost]"
fi
curl -gf -sS "${curlargs[@]}" https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
curl -gf -sS $curlargs https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
if [ $? != 0 ]; then echo $1 failed to download; return 1; fi
}
@@ -108,12 +108,6 @@ run_remote_parts() {
unset confluentscripttmpdir
}
set_selinux_context() {
if [ -x /usr/bin/chcon ]; then
/usr/bin/chcon "$1" "$2"
fi
}
source_remote() {
set_confluent_vars
unsettmpdir=0
@@ -156,7 +150,9 @@ run_remote() {
if [ $? != 0 ]; then echo $requestedcmd failed to download; return 1; fi
chmod +x $1
cmd=$1
set_selinux_context system_u:object_r:bin_t:s0 "$cmd"
if [ -x /usr/bin/chcon ]; then
chcon system_u:object_r:bin_t:s0 $cmd
fi
shift
./$cmd $*
retcode=$?
@@ -175,9 +171,8 @@ run_remote() {
run_remote_python() {
echo
set_confluent_vars
curlargs=(-x "")
if [ -f /etc/confluent/ca.pem ]; then
curlargs=(-x "" --cacert /etc/confluent/ca.pem)
curlargs=" --cacert /etc/confluent/ca.pem"
fi
echo '---------------------------------------------------------------------------'
echo Running python script "'$*'" from https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/
@@ -190,7 +185,7 @@ run_remote_python() {
if [[ "$whost" == *:* ]] && [[ "$whost" != *[* ]] ; then
whost="[$whost]"
fi
curl -gf -sS "${curlargs[@]}" https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
curl -gf -sS $curlargs https://$whost/confluent-public/os/$confluent_profile/scripts/$1 > $1
if [ $? != 0 ]; then echo "'$*'" failed to download; return 1; fi
confluentpython $*
retcode=$?
@@ -217,106 +212,5 @@ run_remote_config() {
echo '---------------------------------------------------------------------------'
return
}
startlegacyprogress() {
[ -n "$progresspid" ] && return
echo -en "Decrypting and extracting root filesystem: 0%\r"
srcsz=$(du -sk /mnt/remote | awk '{print $1}')
while [ -f /mnt/remoteimg/rootimg.sfs ]; do
dstsz=$(du -sk /sysroot | awk '{print $1}')
pct=$((dstsz * 100 / srcsz))
if [ $pct -gt 99 ]; then
pct=99
fi
echo -en "Decrypting and extracting root filesystem: $pct%\r"
sleep 0.25
done &
progresspid=$!
}
extractmultisquash() {
multisquashrc=1
while read -r _ partsrc partmount; do
normalizedmount=${partmount%/}
[ "$normalizedmount" = /mnt/remote ] || continue
if unsquashfs -force -d /sysroot "$partsrc"; then
multisquashrc=0
fi
break
done < /tmp/mountparts.sh
[ $multisquashrc -eq 0 ] || return 1
while read -r _ partsrc partmount; do
normalizedmount=${partmount%/}
[ "$normalizedmount" = /mnt/remote ] && continue
partdest=/sysroot${normalizedmount#/mnt/remote}
mkdir -p "$partdest"
if ! unsquashfs -force -d "$partdest" "$partsrc"; then
return 1
fi
done < /tmp/mountparts.sh
}
cleanupremotemounts() {
if [ -f /tmp/mountparts.sh ]; then
for partmount in $(awk '{ mounts[NR] = $3 } END { for (idx = NR; idx > 0; idx--) print mounts[idx] }' /tmp/mountparts.sh); do
umount "$partmount"
done
for partdev in $(awk '{ devices[NR] = $NF } END { for (idx = NR; idx > 0; idx--) print devices[idx] }' /tmp/setupmount.sh); do
dmsetup remove "$partdev"
done
else
umount /mnt/remote
fi
}
# Extract an untethered/uncompressed diskless root image into /sysroot and
# tear down the source devices; expects rootimgformat, mountsrc, and loopdev
# from imageboot.sh and the image content mounted under /mnt/remote.
extract_untethered_rootimg() {
extractrc=1
progresspid=
if [ "$rootimgformat" = squashfs ] && command -v unsquashfs > /dev/null 2>&1; then
echo "Decrypting and extracting root filesystem in parallel"
if unsquashfs -force -d /sysroot "$mountsrc"; then
extractrc=0
else
echo "Parallel root filesystem extraction failed, retrying with cp"
fi
elif [ "$rootimgformat" = confluent_multisquash ] && command -v unsquashfs > /dev/null 2>&1; then
echo "Decrypting and extracting multipart root filesystem in parallel"
if extractmultisquash; then
extractrc=0
else
echo "Parallel multipart root filesystem extraction failed, retrying with cp"
fi
fi
if [ $extractrc -ne 0 ]; then
rm -rf /sysroot/* /sysroot/.[!.]* /sysroot/..?*
startlegacyprogress
if cp -a /mnt/remote/. /sysroot/; then
extractrc=0
fi
fi
if [ $extractrc -ne 0 ]; then
if [ -n "$progresspid" ]; then
kill "$progresspid" 2> /dev/null || true
wait "$progresspid" 2> /dev/null || true
progresspid=
fi
echo "Failed to extract the root filesystem"
fi
cleanupremotemounts
if [ -e /dev/mapper/cryptimg ]; then
dmsetup remove cryptimg
fi
losetup -d $loopdev
rm /mnt/remoteimg/rootimg.sfs
umount /mnt/remoteimg
if [ -n "$progresspid" ]; then
wait "$progresspid"
echo -e "Decrypting and extracting root filesystem: 100%"
fi
return $extractrc
}
#If invoked as a command, use the arguments to actually run a function
(return 0 2>/dev/null) || $1 "${@:2}"
@@ -9,7 +9,7 @@ mgr=$(cat /etc/confluent/deployer)
cryptboot=$(grep encryptboot: $deploycfg|sed -e 's/^encryptboot: //')
if [ "$cryptboot" != "" ] && [ "$cryptboot" != "none" ] && [ "$cryptboot" != "null" ]; then
echo "****Encrypted boot requested, but not implemented for this OS, halting install" > /dev/console
[ -f '/tmp/autoconsdev' ] && (echo "****Encrypted boot requested, but not implemented for this OS, halting install" >> $(cat /tmp/autoconsdev))
[ -f '/tmp/autoconsdev' ] && (echo "****Encryptod boot requested, but not implemented for this OS,halting install" >> $(cat /tmp/autoconsdev))
while :; do sleep 86400; done
fi
cat > /usr/lib/live-installer.d/confluent-certs << EOF
@@ -34,8 +34,8 @@ done
if [ -e /tmp/installdisk ]; then
instdisk=$(cat /tmp/installdisk)
else
for blockdev in /sys/class/block/*; do
shortname=$(basename "$blockdev")
for blockdev in $(ls /sys/class/block/); do
shortname=$(basename $blockdev)
if [ "$shortname" != "${shortname%loop*}" ]; then
continue
fi
@@ -62,7 +62,7 @@ else
done
fi
if [ -z "$instdisk" ]; then
if [ ! -z "$sraid" ]; then
if [ ! -z "$sraid"]; then
instdisk=$sraid
elif [ ! -z "$onbdisk" ]; then
instdisk=$onbdisk
@@ -1,31 +1,3 @@
pcrextendvalue=2fbe96c50dde38ce9cd2764ddb79c216cfbcd3499568b1125450e60c45dd19f2
pcrhashalgo=sha256
set_tpm_hashalgo() {
if [ -n "$confluent_tpm_hashalgo" ]; then
return 0
fi
tpm_pcrbanks=$(tpm2_getcap pcrs 2>/dev/null)
for algo in sha256 sha512 sha384; do
# Match only banks that actually have PCRs allocated (a digit in [ ... ]).
if echo "$tpm_pcrbanks" | grep -Eq "$algo:[[:space:]]*\[[^]]*[0-9]"; then
confluent_tpm_hashalgo="$algo"
pcrhashalgo="$algo"
if [[ "$algo" == "sha256" ]]; then
pcrextendvalue=2fbe96c50dde38ce9cd2764ddb79c216cfbcd3499568b1125450e60c45dd19f2
elif [[ "$algo" == "sha384" ]]; then
pcrextendvalue=20aaa1073c215c8bc97ff8dc509dd63ff09eef9d2dfa4d8ef224ce372d80e5417e53840ef2fb72924c195f69396a27b9
elif [[ "$algo" == "sha512" ]]; then
pcrextendvalue=77113aa32ac249789c0cdcf24e78efdb81d5be0d878e9a9a750446ecf9b9b3b1c084194eb6187cfd890b8f61a7f2e79eb4d33f6f27827b862367897c8123bceb
fi
return 0
fi
done
return 1
}
get_remote_apikey() {
while [ -z "$confluent_apikey" ]; do
/opt/confluent/bin/clortho $nodename $confluent_mgr > /etc/confluent/confluent.apikey
@@ -42,10 +14,9 @@ get_remote_apikey() {
tmpdir=$(mktemp -d)
cd $tmpdir
tpm2_startauthsession --session=session.ctx
set_tpm_hashalgo
tpm2_policypcr -Q --session=session.ctx --pcr-list="${pcrhashalgo}:15" --policy=pcr15.${pcrhashalgo}.policy
tpm2_policypcr -Q --session=session.ctx --pcr-list="sha256:15" --policy=pcr15.sha256.policy
tpm2_createprimary -G ecc -Q --key-context=prim.ctx
(echo -n "CONFLUENT_APIKEY:";cat /etc/confluent/confluent.apikey) | tpm2_create -Q --policy=pcr15.${pcrhashalgo}.policy --public=data.pub --private=data.priv -i - -C prim.ctx
(echo -n "CONFLUENT_APIKEY:";cat /etc/confluent/confluent.apikey) | tpm2_create -Q --policy=pcr15.sha256.policy --public=data.pub --private=data.priv -i - -C prim.ctx
tpm2_load -Q --parent-context=prim.ctx --public=data.pub --private=data.priv --name=confluent.apikey --key-context=data.ctx
tpm2_evictcontrol -Q -c data.ctx
tpm2_flushcontext session.ctx
@@ -54,7 +25,6 @@ get_remote_apikey() {
fi
done
}
root=1
rootok=1
netroot=confluent
@@ -72,9 +42,7 @@ if ! grep console= /proc/cmdline >& /dev/null; then
autocons=$(/opt/confluent/bin/autocons)
autoconsdev=${autocons%,*}
autocons=${autocons##*/}
if [ ! -z "$autocons" ]; then
echo "Automatic console configured for $autocons"
fi
echo "Automatic console configured for $autocons"
fi
echo "Initializing confluent diskless environment"
echo -n "udevd: "
@@ -110,8 +78,7 @@ lasthdl=""
if [ -c /dev/tpmrm0 ]; then
for hdl in $(tpm2_getcap handles-persistent|awk '{print $2}'); do
tpm2_startauthsession --policy-session --session=session.ctx
set_tpm_hashalgo
tpm2_policypcr -Q --session=session.ctx --pcr-list="${pcrhashalgo}:15" --policy=pcr15.${pcrhashalgo}.policy
tpm2_policypcr -Q --session=session.ctx --pcr-list="sha256:15" --policy=pcr15.sha256.policy
unsealeddata=$(tpm2_unseal --auth=session:session.ctx -Q -c $hdl 2>/dev/null)
tpm2_flushcontext session.ctx
if [[ $unsealeddata == "CONFLUENT_APIKEY:"* ]]; then
@@ -176,8 +143,8 @@ while [ $ready = "0" ]; do
elif grep 'SSL' $tmperr > /dev/null; then
confluent_mgr=${confluent_mgr#[}
confluent_mgr=${confluent_mgr%]}
echo 'Failure establishing TLS connection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)'
if [ ! -z "$autoconsdev" ]; then echo 'Failure establishing TLS connection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)' > $autoconsdev; fi
echo 'Failure establishing TLS conneection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)'
if [ ! -z "$autoconsdev" ]; then echo 'Failure establishing TLS conneection to '$confluent_mgr' (try `osdeploy initialize -t` on the deployment server)' > $autoconsdev; fi
sleep 10
else
ready=1
@@ -186,8 +153,7 @@ while [ $ready = "0" ]; do
done
if [ ! -z "$autocons" ] && grep "textconsole: true" /etc/confluent/confluent.deploycfg > /dev/null; then /opt/confluent/bin/autocons -c > /dev/null; fi
if [ -c /dev/tpmrm0 ]; then
set_tpm_hashalgo
tpm2_pcrextend 15:${pcrhashalgo}=${pcrextendvalue}
tpm2_pcrextend 15:sha256=2fbe96c50dde38ce9cd2764ddb79c216cfbcd3499568b1125450e60c45dd19f2
fi
umask $oldumask
mkdir -p /run/NetworkManager/system-connections
@@ -0,0 +1 @@
../../../../el9-diskless/profiles/default/scripts/functions
@@ -0,0 +1 @@
../../../../el9-diskless/profiles/default/scripts/getinstalldisk
@@ -4,7 +4,6 @@ if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
confluent_whost="[$confluent_mgr]"
fi
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay /sysroot
set_selinux_context system_u:object_r:bin_t:s0 /opt/confluent/bin/urlmount
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
mount -t tmpfs untethered /mnt/remoteimg
curl https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
@@ -30,10 +29,9 @@ if grep '^Format: confluent_crypted' /tmp/rootimg.info > /dev/null; then
mountsrc=/dev/mapper/cryptimg
fi
rootimgformat=$(awk -F': ' '/^Format:/ {print $2; exit}' /tmp/rootimg.info)
if [ "$rootimgformat" = squashfs ]; then
if grep '^Format: squashfs' /tmp/rootimg.info > /dev/null; then
mount -o ro $mountsrc /mnt/remote
elif [ "$rootimgformat" = confluent_multisquash ]; then
elif grep '^Format: confluent_multisquash' /tmp/rootimg.info; then
tail -n +3 /tmp/rootimg.info | awk '{gsub("/", "_"); print "echo 0 " $4 " linear '$mountsrc' " $3 " | dmsetup create mproot" $7}' > /tmp/setupmount.sh
. /tmp/setupmount.sh
cat /tmp/setupmount.sh |awk '{printf "mount /dev/mapper/"$NF" "; sub("mproot", ""); gsub("_", "/"); print "/mnt/remote"$NF}' > /tmp/mountparts.sh
@@ -56,7 +54,27 @@ if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(get
else
mount -t tmpfs disklessroot /sysroot
fi
extract_untethered_rootimg
echo -en "Decrypting and extracting root filesystem: 0%\r"
srcsz=$(du -sk /mnt/remote | awk '{print $1}')
while [ -f /mnt/remoteimg/rootimg.sfs ]; do
dstsz=$(du -sk /sysroot | awk '{print $1}')
pct=$((dstsz * 100 / srcsz))
if [ $pct -gt 99 ]; then
pct=99
fi
echo -en "Decrypting and extracting root filesystem: $pct%\r"
sleep 0.25
done &
cp -ax /mnt/remote/* /sysroot/
umount /mnt/remote
if [ -e /dev/mapper/cryptimg ]; then
dmsetup remove cryptimg
fi
losetup -d $loopdev
rm /mnt/remoteimg/rootimg.sfs
umount /mnt/remoteimg
wait
echo -e "Decrypting and extracting root filesystem: 100%"
else
TETHERED=1
mount -o discard /dev/zram0 /mnt/overlay
@@ -132,9 +150,6 @@ mkdir -p /sysroot/opt/confluent/bin
curl -sf https://$confluent_whost/confluent-public/os/$confluent_profile/scripts/onboot.sh > /sysroot/opt/confluent/bin/onboot.sh
chmod +x /sysroot/opt/confluent/bin/onboot.sh
cp /opt/confluent/bin/apiclient /sysroot/opt/confluent/bin
set_selinux_context system_u:object_r:systemd_unit_file_t:s0 /sysroot/etc/systemd/system/onboot.service
set_selinux_context system_u:object_r:bin_t:s0 /sysroot/opt/confluent/bin/onboot.sh
set_selinux_context system_u:object_r:bin_t:s0 /sysroot/opt/confluent/bin/apiclient
ln -s /etc/systemd/system/onboot.service /sysroot/etc/systemd/system/multi-user.target.wants/onboot.service
cp /etc/confluent/functions /sysroot/etc/confluent/functions
if grep installtodisk /proc/cmdline > /dev/null; then
@@ -144,9 +159,7 @@ if grep installtodisk /proc/cmdline > /dev/null; then
fi
mv /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs
ln -s /sysroot/lib/modules/$(uname -r) /lib/modules/
if [ -d /lib/firmware ]; then
mv /lib/firmware /lib/firmware-ramfs
fi
mv /lib/firmware /lib/firmware-ramfs
ln -s /sysroot/lib/firmware /lib/firmware
kill $(grep -l ^/usr/lib/systemd/systemd-udevd /proc/*/cmdline|cut -d/ -f 3)
if [ $TETHERED -eq 1 ]; then
@@ -165,9 +178,9 @@ if [ $TETHERED -eq 1 ]; then
else
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
fi
set_selinux_context system_u:object_r:root_t:s0 /sysroot
if grep debugssh /proc/cmdline >& /dev/null; then
exec /opt/confluent/bin/start_root
else
exec /opt/confluent/bin/start_root -s # share mount namespace, keep kernel callbacks intact
fi
@@ -0,0 +1 @@
../../../../el9-diskless/profiles/default/scripts/syncfileclient

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