├── codecov.yml ├── .github ├── packaging │ └── project │ │ ├── test │ │ ├── test_execute.bats │ │ └── gha-test-main.sh │ │ ├── gha-main.sh │ │ └── build_package.sh ├── dependabot.yml └── workflows │ ├── format_check.yml │ ├── draft-release.yml │ ├── snyk-test.yml │ ├── tests.yml │ ├── codeql.yml │ ├── mac-artifact.yml │ └── build-artifacts.yml ├── tox.ini ├── test ├── e2e │ ├── live_cluster │ │ ├── test_help.py │ │ ├── test_version.py │ │ ├── __init__.py │ │ └── test_watch.py │ ├── __init__.py │ ├── aerospike_6.x.conf │ └── aerospike_latest.conf ├── __init__.py ├── unit │ ├── __init__.py │ ├── utils │ │ ├── __init__.py │ │ └── test_lookup_dict.py │ ├── view │ │ ├── __init__.py │ │ └── test_templates.py │ ├── health │ │ ├── __init__.py │ │ └── test_commands.py │ ├── live_cluster │ │ ├── __init__.py │ │ └── client │ │ │ ├── __init__.py │ │ │ └── test_msgpack.py │ ├── collectinfo_analyzer │ │ ├── __init__.py │ │ ├── test_log_handler.py │ │ └── test_show_controller.py │ └── util.py └── test_asinfo.sh ├── .gitmodules ├── .build.yml ├── lib ├── __init__.py ├── health │ ├── __init__.py │ ├── exceptions.py │ ├── errors.py │ ├── constants.py │ └── commands.py ├── utils │ ├── __init__.py │ ├── async_object.py │ ├── types.py │ ├── timeout.py │ ├── file_size.py │ ├── data.py │ ├── log_util.py │ ├── logger.py │ └── constants.py ├── view │ ├── __init__.py │ ├── terminal │ │ ├── __init__.py │ │ └── get_terminal_size.py │ └── sheet │ │ ├── source.py │ │ ├── const.py │ │ ├── render │ │ ├── render_utils │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── json_rsheet.py │ │ └── __init__.py ├── live_cluster │ ├── __init__.py │ ├── live_cluster_command_controller.py │ ├── client │ │ ├── constants.py │ │ ├── __init__.py │ │ ├── ctx.py │ │ ├── msgpack.py │ │ └── ssl_util.py │ ├── constants.py │ ├── pager_controller.py │ ├── features_controller.py │ └── asinfo_controller.py ├── log_analyzer │ ├── __init__.py │ ├── log_handler │ │ ├── __init__.py │ │ └── util.py │ └── log_analyzer_command_controller.py ├── collectinfo_analyzer │ ├── __init__.py │ ├── collectinfo_handler │ │ ├── __init__.py │ │ └── collectinfo_parser │ │ │ └── __init__.py │ ├── collectinfo_command_controller.py │ ├── list_controller.py │ ├── page_controller.py │ ├── collectinfo_root_controller.py │ ├── features_controller.py │ └── summary_controller.py └── base_get_controller.py ├── Pipfile ├── .gitignore ├── pkg ├── Makefile └── astools.conf ├── makefile ├── asinfo ├── run_asinfo_test.sh └── asinfo.txt └── README.md /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | bot: jdogmcsteezy 3 | -------------------------------------------------------------------------------- /.github/packaging/project/test/test_execute.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "can run asadm" { 4 | asadm --help 5 | [ "$?" -eq 0 ] 6 | } -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | disable_warnings = true 3 | parallel = true 4 | 5 | [coverage:report] 6 | include = lib/*, asadm.py 7 | 8 | [flake8] 9 | max-line-length = 88 10 | extend-ignore = E203, E501, E266 11 | -------------------------------------------------------------------------------- /.github/packaging/project/test/gha-test-main.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | DISTRO="$1" 4 | REPO_NAME="$2" 5 | env 6 | git fetch --unshallow --tags --no-recurse-submodules 7 | .github/packaging/common/example-test.sh "$DISTRO" "$REPO_NAME" -------------------------------------------------------------------------------- /test/e2e/live_cluster/test_help.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from test.e2e import util 3 | 4 | 5 | class VersionTests(unittest.TestCase): 6 | def test_return_code(self): 7 | args = "--help" 8 | o = util.run_asadm(args) 9 | self.assertEqual(o.returncode, 0) 10 | -------------------------------------------------------------------------------- /test/e2e/live_cluster/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from test.e2e import util 3 | 4 | 5 | class VersionTests(unittest.TestCase): 6 | def test_return_code(self): 7 | args = "--version" 8 | o = util.run_asadm(args) 9 | self.assertEqual(o.returncode, 0) 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/live_cluster/client/schemas"] 2 | path = lib/live_cluster/client/schemas 3 | url = https://github.com/aerospike/schemas.git 4 | [submodule ".github/packaging/common"] 5 | path = .github/packaging/common 6 | url = https://github.com/aerospike/tools-packaging-common.git 7 | -------------------------------------------------------------------------------- /.github/packaging/project/gha-main.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | DISTRO="$1" 5 | env 6 | cd local 7 | git fetch --unshallow --tags --no-recurse-submodules 8 | git submodule update --init 9 | ls -laht 10 | git branch -v 11 | .github/packaging/common/entrypoint.sh -c -d $DISTRO 12 | .github/packaging/common/entrypoint.sh -e -d $DISTRO 13 | ls -laht ../dist 14 | -------------------------------------------------------------------------------- /.build.yml: -------------------------------------------------------------------------------- 1 | name: aerospike-admin 2 | 3 | container: 4 | - base: 5 | - docker.qe.aerospike.com/build/aerospike-admin:pyinstaller 6 | - docker.qe.aerospike.com/build/aerospike-admin:arm-pyinstaller 7 | 8 | build: 9 | - name: build 10 | script: 11 | - make 12 | - tar -C /work/source/build/bin -czf /work/source/build/bin/asadm.tgz asadm/ 13 | artifact: 14 | - /work/source/build/bin/asadm.tgz 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/health/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/view/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/e2e/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit/view/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/live_cluster/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/log_analyzer/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit/health/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/e2e/live_cluster/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit/live_cluster/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/log_analyzer/log_handler/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit/collectinfo_analyzer/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /test/unit/live_cluster/client/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/collectinfo_handler/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/view/terminal/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .get_terminal_size import get_terminal_size 16 | from .terminal import * 17 | -------------------------------------------------------------------------------- /.github/packaging/project/build_package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | function build_packages(){ 5 | if [ "$ENV_DISTRO" = "" ]; then 6 | echo "ENV_DISTRO is not set" 7 | return 8 | fi 9 | GIT_DIR=$(git rev-parse --show-toplevel) 10 | cd "$GIT_DIR" 11 | make one-file 12 | cd $GIT_DIR/pkg 13 | echo "building package for $BUILD_DISTRO" 14 | 15 | if [[ $ENV_DISTRO == *"ubuntu"* ]]; then 16 | make deb 17 | elif [[ $ENV_DISTRO == *"debian"* ]]; then 18 | make deb 19 | elif [[ $ENV_DISTRO == *"el"* ]]; then 20 | make rpm 21 | elif [[ $ENV_DISTRO == *"amzn"* ]]; then 22 | make rpm 23 | else 24 | make tar 25 | fi 26 | 27 | mkdir -p /tmp/output/$ENV_DISTRO 28 | cp -a $GIT_DIR/pkg/target/* /tmp/output/$ENV_DISTRO 29 | } -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | bcrypt = "4.0.1" 8 | cryptography = "==44.0.1" 9 | pyopenssl = "==25.0.0" 10 | distro = "==1.9.0" 11 | jsonschema = "==4.23.0" 12 | pexpect = "==4.9.0" 13 | ply = "==3.11" 14 | pyasn1 = "==0.6.1" 15 | toml = "==0.10.2" 16 | yappi = "==1.6.0" 17 | setuptools = "80.3.1" 18 | aiohttp = "==3.12.15" 19 | python-dateutil = "2.8.2" 20 | msgpack = "1.0.7" 21 | parameterized = "*" 22 | typing-extensions = "*" 23 | asyncssh = "*" 24 | 25 | [dev-packages] 26 | parameterized = "*" 27 | pyinstaller = "==6.15.0" 28 | black = "25.1.0" 29 | flake8 = "7.3.0" 30 | pytest = "*" 31 | mock = "*" 32 | coverage = "*" 33 | macholib = {version = "*", sys_platform = "== 'darwin'"} 34 | asynctest = "*" 35 | aerospike = "*" 36 | docker = "*" 37 | 38 | [requires] 39 | python_version = "3.10" 40 | 41 | [pipenv] 42 | allow_prereleases = true 43 | -------------------------------------------------------------------------------- /lib/health/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class HealthException(Exception): 17 | def __call__(self, *ignore): 18 | # act as a callable and raise self 19 | raise self 20 | 21 | 22 | class SyntaxException(Exception): 23 | def __call__(self, *ignore): 24 | # act as a callable and raise self 25 | raise self 26 | -------------------------------------------------------------------------------- /lib/live_cluster/live_cluster_command_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from lib.base_controller import CommandController 15 | from .client import Cluster 16 | 17 | 18 | class LiveClusterCommandController(CommandController): 19 | cluster: Cluster = None 20 | 21 | def __init__(self, cluster: Cluster): 22 | LiveClusterCommandController.cluster = cluster 23 | -------------------------------------------------------------------------------- /lib/live_cluster/client/constants.py: -------------------------------------------------------------------------------- 1 | class ErrorsMsgs: 2 | NS_DNE = "Namespace does not exist" 3 | DC_DNE = "DC does not exist" 4 | UDF_DNE = "UDF does not exist" 5 | DC_EXISTS = "DC already exists" 6 | UDF_UPLOAD_FAIL = "Failed to add UDF" 7 | ROSTER_READ_FAIL = "Could not retrieve roster for namespace" 8 | DC_CREATE_FAIL = "Failed to create XDR datacenter" 9 | DC_DELETE_FAIL = "Failed to delete XDR datacenter" 10 | DC_NS_ADD_FAIL = "Failed to add namespace to XDR datacenter" 11 | DC_NS_REMOVE_FAIL = "Failed to remove namespace from XDR datacenter" 12 | DC_NODE_ADD_FAIL = "Failed to add node to XDR datacenter" 13 | DC_NODE_REMOVE_FAIL = "Failed to remove node from XDR datacenter" 14 | INVALID_REWIND = 'Invalid rewind. Must be int or "all"' 15 | INFO_SERVER_ERROR_RESPONSE = "Server returned an error response for info command" 16 | 17 | 18 | DEFAULT_CONFIG_PATH = "/etc/aerospike/aerospike.conf" 19 | MAX_SOCKET_POOL_SIZE = 16 20 | -------------------------------------------------------------------------------- /lib/log_analyzer/log_analyzer_command_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from lib.base_controller import CommandController 16 | from lib.log_analyzer.log_handler.log_handler import LogHandler 17 | 18 | 19 | class LogAnalyzerCommandController(CommandController): 20 | log_handler = None 21 | 22 | def __init__(self, log_handler: LogHandler): 23 | LogAnalyzerCommandController.log_handler = log_handler 24 | -------------------------------------------------------------------------------- /lib/view/sheet/source.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def source_hierarchy(source_path): 17 | return source_path.split(".") 18 | 19 | 20 | def source_root(source_path): 21 | return source_hierarchy(source_path)[0] 22 | 23 | 24 | def source_lookup(sources, source_path): 25 | cur_node = sources 26 | 27 | for node in source_hierarchy(source_path): 28 | cur_node = cur_node[node] 29 | 30 | return cur_node 31 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/collectinfo_command_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from lib.base_controller import CommandController 16 | from lib.collectinfo_analyzer.collectinfo_handler.log_handler import ( 17 | CollectinfoLogHandler, 18 | ) 19 | 20 | 21 | class CollectinfoCommandController(CommandController): 22 | def __init__(self, log_handler: CollectinfoLogHandler): 23 | CollectinfoCommandController.log_handler = log_handler 24 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/collectinfo_handler/collectinfo_parser/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Set default handler to avoid "No handler found" warnings. 16 | import logging 17 | 18 | try: # Python 2.7+ 19 | from logging import NullHandler 20 | except ImportError: 21 | 22 | class NullHandler(logging.Handler): 23 | def emit(self, record): 24 | pass 25 | 26 | 27 | # logging.getLogger(__name__).addHandler(NullHandler()) 28 | -------------------------------------------------------------------------------- /lib/live_cluster/client/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .types import ( 16 | ASInfoError, 17 | ASInfoResponseError, 18 | ASInfoNotAuthenticatedError, 19 | ASInfoClusterStableError, 20 | ASInfoConfigError, 21 | ASProtocolError, 22 | ASResponse, 23 | ASINFO_RESPONSE_OK, 24 | Addr_Port_TLSName, 25 | ) 26 | 27 | from .config_handler import ( 28 | BoolConfigType, 29 | EnumConfigType, 30 | StringConfigType, 31 | IntConfigType, 32 | ) 33 | 34 | from .cluster import Cluster 35 | 36 | from .ctx import CTXItem, CTXItems, CDTContext, ASValue, ASValues 37 | -------------------------------------------------------------------------------- /.github/workflows/format_check.yml: -------------------------------------------------------------------------------- 1 | name: Format Check 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: ["dependabot/**"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Harden the runner (Audit all outbound calls) 18 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 19 | with: 20 | egress-policy: audit 21 | 22 | - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 23 | with: 24 | submodules: true 25 | - name: Get Python version from Pipfile 26 | run: echo "PYTHON_VERSION=$(grep "python_version" Pipfile | cut -d ' ' -f 3 - | tr -d '"')" >> $GITHUB_ENV 27 | 28 | - name: Set up Python ${{ env.PYTHON_VERSION }} 29 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 30 | with: 31 | python-version: ${{ env.PYTHON_VERSION }} 32 | 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install pipenv 36 | pipenv install --dev 37 | 38 | - name: Run Format Check 39 | run: | 40 | make format-check -------------------------------------------------------------------------------- /lib/live_cluster/constants.py: -------------------------------------------------------------------------------- 1 | from lib.base_controller import ModifierHelp 2 | 3 | 4 | SSH_MODIFIER_USAGE = "--enable-ssh [--ssh-user ] [--ssh-pwd ] [--ssh-port ] [--ssh-key ] [--ssh-key-pwd]" 5 | SSH_MODIFIER_HELP = ( 6 | ModifierHelp( 7 | "--enable-ssh", 8 | "Enables the collection of system statistics from a remote server via SSH using configuration loaded from '.ssh/config' by default.", 9 | ), 10 | ModifierHelp( 11 | "--ssh-user", 12 | "User ID to use when logging into remote servers via SSH. If not provided and no matching config file entry is found the current user ID is used.", 13 | default="current user", 14 | ), 15 | ModifierHelp( 16 | "--ssh-pwd", 17 | "User password used for logging into the remote servers via SSH.", 18 | ), 19 | ModifierHelp( 20 | "--ssh-key", 21 | "SSH key file path to send to the remote servers for authentication. If not provided and no config file entry is found then the keys in the .ssh directory are used.", 22 | ), 23 | ModifierHelp( 24 | "--ssh-port", 25 | "SSH port to use when logging into the remote servers.", 26 | default="22", 27 | ), 28 | ) 29 | -------------------------------------------------------------------------------- /lib/utils/async_object.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Awaitable, Coroutine, Type, TypeVar 16 | 17 | 18 | class AsyncObject(object): 19 | """Inheriting this class allows you to define an async __init__. 20 | 21 | So you can create objects by doing something like `await MyClass(params)` 22 | """ 23 | 24 | Class = TypeVar("Class") 25 | 26 | async def __new__(cls: Type[Class], *args, **kwargs) -> Class: 27 | instance = super().__new__(cls) 28 | await instance.__init__(*args, **kwargs) # type: ignore 29 | return instance 30 | 31 | async def __init__(self): 32 | pass 33 | -------------------------------------------------------------------------------- /lib/view/sheet/const.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class FieldType(object): 17 | boolean = "boolean" 18 | number = "number" 19 | string = "string" 20 | dynamic = "dynamic" 21 | undefined = "undefined" 22 | 23 | 24 | class FieldAlignment(object): 25 | center = "center" 26 | left = "left" 27 | right = "right" 28 | 29 | 30 | class SheetStyle(object): 31 | columns = "columns" 32 | rows = "rows" 33 | json = "json" 34 | 35 | 36 | class DynamicFieldOrder(object): 37 | source = "source" # preserve source's order 38 | ascending = "ascending" # ascending key order 39 | descending = "descending" # descending key order 40 | -------------------------------------------------------------------------------- /lib/view/sheet/render/render_utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class ErrorEntry(object): 17 | def __lt__(self, other): 18 | return True 19 | 20 | def __le__(self, other): 21 | return True 22 | 23 | def __gt__(self, other): 24 | return False 25 | 26 | def __ge__(self, other): 27 | return False 28 | 29 | 30 | class NoEntry(object): 31 | def __lt__(self, other): 32 | return True 33 | 34 | def __le__(self, other): 35 | return True 36 | 37 | def __gt__(self, other): 38 | return False 39 | 40 | def __ge__(self, other): 41 | return False 42 | 43 | 44 | ErrorEntry = ErrorEntry() 45 | NoEntry = NoEntry() 46 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: "Draft Release" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | # workflows to pull release artifacts from 10 | snyk-test: 11 | uses: aerospike/aerospike-admin/.github/workflows/snyk-test.yml@master 12 | draft-release-notes: 13 | needs: snyk-test 14 | runs-on: ubuntu-latest 15 | name: Draft Release 16 | steps: 17 | - name: Harden the runner (Audit all outbound calls) 18 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 19 | with: 20 | egress-policy: audit 21 | 22 | - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 23 | # below steps are for downloading release artifacts 24 | - name: Download Snyk Artifact 25 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 26 | with: 27 | name: asadm-snyk.txt 28 | path: artifacts 29 | # finally create the release and upload artifacts 30 | - name: Upload Artifacts to Release Draft 31 | uses: step-security/action-gh-release@674f8f866246c6b91bd3f1688ba95e27b2ae8d2e # v2.4.1 32 | with: 33 | draft: true 34 | generate_release_notes: true 35 | files: | 36 | artifacts/* 37 | -------------------------------------------------------------------------------- /lib/view/sheet/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .const import DynamicFieldOrder, FieldAlignment, FieldType, SheetStyle 16 | from .decleration import ( 17 | Aggregators, 18 | Converters, 19 | DynamicFields, 20 | Field, 21 | Formatters, 22 | Projectors, 23 | Sheet, 24 | Subgroup, 25 | TitleField, 26 | ) 27 | from .render import render, set_style_json 28 | 29 | __all__ = ( 30 | DynamicFieldOrder, 31 | FieldAlignment, 32 | FieldType, 33 | SheetStyle, 34 | render, 35 | set_style_json, 36 | Aggregators, 37 | Converters, 38 | DynamicFields, 39 | Field, 40 | Formatters, 41 | Projectors, 42 | Sheet, 43 | Subgroup, 44 | TitleField, 45 | ) 46 | -------------------------------------------------------------------------------- /lib/health/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class Errors: 17 | errors = { 18 | "HEALTH_ERROR1": "System Has Low Memory Pct", 19 | "HEALTH_ERROR2": "System Has Low Memory Pct", 20 | "HEALTH_ERROR3": "System lot of connection Churn", 21 | "HEALTH_ERROR4": "Misconfigured Namespace memory sizes", 22 | "HEALTH_ERROR5": "Namespace Low In Disk Avail PCT", 23 | "HEALTH_ERROR6": "Namespace Possible misconfiguration", 24 | "HEALTH_ERROR7": "Namespace Anamolistic Pattern", 25 | "HEALTH_ERROR8": "Set Delete in progress", 26 | } 27 | 28 | @staticmethod 29 | def get_error_discription(e_id): 30 | if e_id in Errors.errors: 31 | return Errors.errors[e_id] 32 | return e_id 33 | -------------------------------------------------------------------------------- /lib/log_analyzer/log_handler/util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # ------------------------------------------------ 17 | # Check line contains strings from strs in given order. 18 | # 19 | def contains_substrings_in_order(line="", strs=[]): 20 | if not strs: 21 | return True 22 | 23 | if not line: 24 | return False 25 | 26 | s_str = strs[0] 27 | if not s_str: 28 | return True 29 | 30 | if s_str in line: 31 | try: 32 | main_str = line.split(s_str, 1)[1] 33 | 34 | except Exception: 35 | main_str = "" 36 | 37 | if len(strs) <= 1: 38 | return True 39 | 40 | return contains_substrings_in_order(main_str, strs[1:]) 41 | 42 | else: 43 | return False 44 | -------------------------------------------------------------------------------- /lib/utils/types.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import TypeVar, TypedDict 16 | from typing_extensions import NotRequired 17 | 18 | T = TypeVar("T") 19 | 20 | # TODO: Could be moved to its own utils.types module. 21 | NodeIP = str 22 | DC = str 23 | NS = str 24 | SET = str 25 | METRIC = str 26 | Users = str 27 | NodeDict = dict[NodeIP, T] 28 | DatacenterDict = dict[DC, T] 29 | NamespaceDict = dict[NS, T] 30 | UsersDict = dict[Users, T] 31 | 32 | 33 | class StopWritesEntry(TypedDict): 34 | metric: str 35 | metric_usage: int | float 36 | stop_writes: bool 37 | metric_threshold: NotRequired[int | float] 38 | config: NotRequired[str] 39 | namespace: NotRequired[str] 40 | set: NotRequired[str] 41 | 42 | 43 | StopWritesEntryKey = tuple[NS, SET, METRIC] 44 | StopWritesDict = NodeDict[dict[StopWritesEntryKey, StopWritesEntry]] 45 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/list_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from lib.view import terminal 16 | from lib.base_controller import CommandHelp 17 | 18 | from .collectinfo_command_controller import CollectinfoCommandController 19 | 20 | 21 | @CommandHelp("Displays all added collectinfos files.") 22 | class ListController(CollectinfoCommandController): 23 | def __init__(self): 24 | self.controller_map = {} 25 | self.modifiers = set() 26 | 27 | def _do_all(self, line): 28 | cinfo_logs = self.log_handler.all_cinfo_logs 29 | for timestamp, snapshot in cinfo_logs.items(): 30 | print( 31 | terminal.bold() 32 | + str(timestamp) 33 | + terminal.unbold() 34 | + ": " 35 | + str(snapshot.cinfo_file) 36 | ) 37 | 38 | def _do_default(self, line): 39 | self._do_all(line) 40 | -------------------------------------------------------------------------------- /lib/live_cluster/pager_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from lib.view.view import CliView 15 | from lib.base_controller import CommandHelp 16 | 17 | from .live_cluster_command_controller import LiveClusterCommandController 18 | 19 | 20 | @CommandHelp("Turn terminal pager on or off") 21 | class PagerController(LiveClusterCommandController): 22 | def __init__(self): 23 | self.modifiers = set() 24 | 25 | @CommandHelp( 26 | "Displays output with vertical and horizontal paging for each output table same as linux 'less' command. Use arrow keys to scroll output and 'q' to end page for table. All linux less commands can work in this pager option.", 27 | short_msg="Enables output paging. Similar to linux 'less'", 28 | ) 29 | def do_on(self, line): 30 | CliView.pager = CliView.LESS 31 | 32 | @CommandHelp("Disables paging and prints output normally") 33 | def do_off(self, line): 34 | CliView.pager = CliView.NO_PAGER 35 | 36 | @CommandHelp("Display output in scrolling mode") 37 | def do_scroll(self, line): 38 | CliView.pager = CliView.SCROLL 39 | -------------------------------------------------------------------------------- /.github/workflows/snyk-test.yml: -------------------------------------------------------------------------------- 1 | name: "Snyk Report" 2 | 3 | on: 4 | workflow_call: 5 | jobs: 6 | security: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Harden the runner (Audit all outbound calls) 10 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 11 | with: 12 | egress-policy: audit 13 | 14 | - name: Checkout repository 15 | uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 16 | - name: Setup snyk CLI 17 | uses: snyk/actions/setup@cdb760004ba9ea4d525f2e043745dfe85bb9077e 18 | with: 19 | snyk-version: v1.1297.3 20 | - name: Get Python version from Pipfile 21 | working-directory: ${{ steps.working-dir.outputs.value }} 22 | run: | 23 | echo "PYTHON_VERSION=$(grep "python_version" Pipfile | cut -d ' ' -f 3 - | tr -d '"')" >> $GITHUB_ENV 24 | - name: Setup Python 25 | uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3.1.4 26 | with: 27 | python-version: ${{ env.PYTHON_VERSION }} 28 | - name: Pipenv setup 29 | run: | 30 | pip install pipenv 31 | pipenv install 32 | - name: Run Snyk to check for vulnerabilities and record dependencies 33 | run: | 34 | snyk test --print-deps | sed -r "s/\x1B\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[m|K]//g" | tee asadm-snyk.txt 35 | env: 36 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 37 | - name: Upload snyk results 38 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 39 | with: 40 | name: asadm-snyk.txt 41 | path: asadm-snyk.txt 42 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/page_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from lib.base_controller import CommandHelp 16 | from lib.view.view import CliView 17 | 18 | from .collectinfo_command_controller import CollectinfoCommandController 19 | 20 | 21 | @CommandHelp("Turn terminal pager on or off") 22 | class PagerController(CollectinfoCommandController): 23 | def __init__(self): 24 | self.modifiers = set() 25 | 26 | @CommandHelp( 27 | "Displays output with vertical and horizontal paging for each output table same as linux 'less' command. Use arrow keys to scroll output and 'q' to end page for table. All linux less commands can work in this pager option.", 28 | short_msg="Enables output paging. Similar to linux 'less'", 29 | ) 30 | def do_on(self, line): 31 | CliView.pager = CliView.LESS 32 | 33 | @CommandHelp("Disables paging and prints output normally") 34 | def do_off(self, line): 35 | CliView.pager = CliView.NO_PAGER 36 | 37 | @CommandHelp("Display output in scrolling mode") 38 | def do_scroll(self, line): 39 | CliView.pager = CliView.SCROLL 40 | -------------------------------------------------------------------------------- /lib/live_cluster/features_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from lib.live_cluster.get_controller import GetFeaturesController 15 | from lib.utils import constants 16 | from lib.base_controller import CommandHelp, ModifierHelp 17 | 18 | from .live_cluster_command_controller import LiveClusterCommandController 19 | 20 | with_modifier_help = ModifierHelp( 21 | constants.Modifiers.WITH, constants.ModifierHelpText.WITH 22 | ) 23 | 24 | 25 | @CommandHelp( 26 | "Lists the features in use in a running Aerospike cluster.", 27 | usage=f"[{constants.Modifiers.LIKE} ] [{constants.ModifierUsage.WITH}]", 28 | modifiers=( 29 | ModifierHelp(constants.Modifiers.LIKE, "Filter features by substring match"), 30 | with_modifier_help, 31 | ), 32 | ) 33 | class FeaturesController(LiveClusterCommandController): 34 | def __init__(self): 35 | self.modifiers = set(["with", "like"]) 36 | self.getter = GetFeaturesController(self.cluster) 37 | 38 | async def _do_default(self, line): 39 | features = await self.getter.get_features(nodes=self.nodes) 40 | self.view.show_config("Features", features, self.cluster, **self.mods) 41 | -------------------------------------------------------------------------------- /test/e2e/live_cluster/test_watch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | import asynctest 18 | 19 | import lib.live_cluster.live_cluster_root_controller as controller 20 | import lib.utils.util as util 21 | from test.e2e import lib, util as test_util 22 | from lib.view.sheet import set_style_json 23 | 24 | set_style_json() 25 | 26 | 27 | class TestWatch(asynctest.TestCase): 28 | async def setUp(self): 29 | lib.start() 30 | self.rc = await controller.LiveClusterRootController( 31 | [(lib.SERVER_IP, lib.PORT, None)], 32 | user="admin", 33 | password="admin", 34 | ) # type: ignore 35 | 36 | def tearDown(self) -> None: 37 | lib.stop() 38 | 39 | async def test_watch(self): 40 | actual_out = await util.capture_stdout( 41 | self.rc.execute, ["watch", "1", "3", "info", "network"] 42 | ) 43 | output_list = test_util.get_separate_output(actual_out) 44 | info_counter = 0 45 | for item in output_list: 46 | if "Network Information" in item["title"]: 47 | info_counter += 1 48 | self.assertEqual(info_counter, 3) 49 | 50 | 51 | if __name__ == "__main__": 52 | # import sys;sys.argv = ['', 'Test.testName'] 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /lib/health/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | HEALTH_PARSER_VAR = "health_parser_var" 16 | MAJORITY = "MAJORITY_VALUE" 17 | 18 | 19 | class AssertLevel: 20 | CRITICAL = 0 21 | WARNING = 1 22 | INFO = 2 23 | 24 | 25 | class AssertResultKey: 26 | FAIL_MSG = "Failmsg" 27 | KEYS = "Keys" 28 | CATEGORY = "Category" 29 | LEVEL = "Level" 30 | DESCRIPTION = "Description" 31 | SUCCESS_MSG = "Successmsg" 32 | SUCCESS = "Success" 33 | 34 | 35 | class ParserResultType: 36 | ASSERT = "assert_result" 37 | 38 | 39 | class HealthResultType: 40 | ASSERT = "assert_summary" 41 | EXCEPTIONS = "exceptions" 42 | EXCEPTIONS_SYNTAX = "syntax" 43 | EXCEPTIONS_PROCESSING = "processing" 44 | EXCEPTIONS_OTHER = "other" 45 | STATUS_COUNTERS = "status_counters" 46 | DEBUG_MESSAGES = "debug_messages" 47 | 48 | 49 | class HealthResultCounter: 50 | QUERY_COUNTER = "queries" 51 | QUERY_SUCCESS_COUNTER = "queries_success" 52 | QUERY_SKIPPED_COUNTER = "queries_skipped" 53 | ASSERT_FAILED_COUNTER = "assert_failed" 54 | ASSERT_PASSED_COUNTER = "assert_passed" 55 | ASSERT_QUERY_COUNTER = "assert_queries" 56 | DEBUG_COUNTER = "debug_prints" 57 | SYNTAX_EXCEPTION_COUNTER = "syntax_exceptions" 58 | HEALTH_EXCEPTION_COUNTER = "health_exceptions" 59 | OTHER_EXCEPTION_COUNTER = "other_exceptions" 60 | -------------------------------------------------------------------------------- /test/unit/util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def assert_exception(self, exception, message, func, *args): 17 | """ 18 | exception: The exception type you want thrown. 19 | message: The message given to the exception when raised, None to not check message 20 | func: Function to run 21 | args: Arguments to func 22 | """ 23 | exc = None 24 | 25 | try: 26 | func(*args) 27 | except Exception as e: 28 | exc = e 29 | 30 | self.assertIsNotNone(exc, "No exception thrown") 31 | self.assertIsInstance(exc, exception, "Wrong exception type") 32 | 33 | if message is not None: 34 | self.assertEqual(str(exc), message, "Correct exception but wrong message") 35 | 36 | 37 | async def assert_exception_async(self, exception, message, func, *args): 38 | """ 39 | exception: The exception type you want thrown. 40 | message: The message given to the exception when raised, None to not check message 41 | func: Function to run 42 | args: Arguments to func 43 | """ 44 | exc = None 45 | 46 | try: 47 | await func(*args) 48 | except Exception as e: 49 | exc = e 50 | 51 | self.assertIsNotNone(exc, "No exception thrown") 52 | self.assertIsInstance(exc, exception, "Wrong exception type") 53 | 54 | if message is not None: 55 | self.assertEqual(str(exc), message, "Correct exception but wrong message") 56 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: ["*"] 5 | pull_request: 6 | branches: ["dependabot/**"] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Harden the runner (Audit all outbound calls) 12 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 13 | with: 14 | egress-policy: audit 15 | 16 | - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 17 | with: 18 | submodules: true 19 | - name: Get Python version from Pipfile 20 | run: echo "PYTHON_VERSION=$(grep "python_version" Pipfile | cut -d ' ' -f 3 - | tr -d '"')" >> $GITHUB_ENV 21 | - name: Set up Python ${{ env.PYTHON_VERSION }} 22 | uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3.1.4 23 | with: 24 | python-version: ${{ env.PYTHON_VERSION }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install pipenv 28 | pipenv install --dev 29 | - name: Build 30 | run: | 31 | make 32 | - name: Tests with coverage report 33 | env: 34 | FEATKEY: ${{ secrets.TEST_FEAT_KEY }} 35 | run: | 36 | pipenv run bash -c "make coverage" 37 | pipenv run bash -c "coverage xml" 38 | - name: Upload Collectinfo for Debugging 39 | if: failure() 40 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 41 | with: 42 | name: test_collectinfo 43 | path: /tmp/asadm_test* 44 | if-no-files-found: error 45 | - name: Upload Health Struct for Debugging 46 | if: failure() 47 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 48 | with: 49 | name: health_files 50 | path: "*_health_input.txt" 51 | if-no-files-found: error 52 | - name: Upload coverage report to Codecov 53 | uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3.1.6 54 | with: 55 | files: coverage.xml 56 | fail_ci_if_error: false 57 | -------------------------------------------------------------------------------- /lib/utils/timeout.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import signal 16 | import subprocess 17 | 18 | DEFAULT_TIMEOUT = 5.0 19 | 20 | 21 | class TimeoutException(Exception): 22 | """A timeout has occurred.""" 23 | 24 | pass 25 | 26 | 27 | class call_with_timeout: 28 | def __init__(self, function, timeout=DEFAULT_TIMEOUT): 29 | self.timeout = timeout 30 | self.function = function 31 | 32 | def handler(self, signum, frame): 33 | raise TimeoutException() 34 | 35 | def __call__(self, *args): 36 | # get the old SIGALRM handler 37 | old = signal.signal(signal.SIGALRM, self.handler) 38 | # set the alarm 39 | signal.setitimer(signal.ITIMER_REAL, self.timeout) 40 | try: 41 | result = self.function(*args) 42 | finally: 43 | # restore existing SIGALRM handler 44 | signal.signal(signal.SIGALRM, old) 45 | signal.setitimer(signal.ITIMER_REAL, 0) 46 | return result 47 | 48 | 49 | def timeout(timeout): 50 | """This decorator takes a timeout parameter in seconds.""" 51 | 52 | def wrap_function(function): 53 | return call_with_timeout(function, timeout) 54 | 55 | return wrap_function 56 | 57 | 58 | def default_timeout(function): 59 | """This simple decorator 'timesout' after DEFAULT_TIMEOUT seconds.""" 60 | return call_with_timeout(function) 61 | 62 | 63 | def getstatusoutput(command, timeout=DEFAULT_TIMEOUT): 64 | """This is a timeout wrapper around getstatusoutput.""" 65 | _gso = call_with_timeout(subprocess.getstatusoutput, timeout) 66 | try: 67 | return _gso(command) 68 | except TimeoutException: 69 | return (-1, "The command '%s' timed-out after %i seconds." % (command, timeout)) 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Intellij files 2 | .idea 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/bin 15 | build/tmp 16 | build/asadm 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # Futurize backup files 29 | *.bak 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | [._]*.s[a-w][a-z] 62 | [._]s[a-w][a-z] 63 | *.un~ 64 | Session.vim 65 | .netrwhist 66 | *~ 67 | 68 | .DS_Store 69 | .AppleDouble 70 | .LSOverride 71 | 72 | # Icon must end with two \r 73 | Icon 74 | 75 | 76 | # Thumbnails 77 | ._* 78 | 79 | # Files that might appear on external disk 80 | .Spotlight-V100 81 | .Trashes 82 | 83 | # Directories potentially created on remote AFP share 84 | .AppleDB 85 | .AppleDesktop 86 | Network Trash Folder 87 | Temporary Items 88 | .apdisk 89 | 90 | # -*- mode: gitignore; -*- 91 | *~ 92 | \#*\# 93 | /.emacs.desktop 94 | /.emacs.desktop.lock 95 | *.elc 96 | auto-save-list 97 | tramp 98 | .\#* 99 | 100 | # Org-mode 101 | .org-id-locations 102 | *_archive 103 | 104 | # flymake-mode 105 | *_flymake.* 106 | 107 | # eshell files 108 | /eshell/history 109 | /eshell/lastdir 110 | 111 | # elpa packages 112 | /elpa/ 113 | 114 | # reftex files 115 | *.rel 116 | 117 | # AUCTeX auto folder 118 | /auto/ 119 | 120 | # env 121 | .env 122 | 123 | # -*- mode: gitignore; -*- 124 | *~ 125 | \#*\# 126 | /.emacs.desktop 127 | /.emacs.desktop.lock 128 | *.elc 129 | auto-save-list 130 | tramp 131 | .\#* 132 | 133 | # Org-mode 134 | .org-id-locations 135 | *_archive 136 | 137 | # flymake-mode 138 | *_flymake.* 139 | 140 | # eshell files 141 | /eshell/history 142 | /eshell/lastdir 143 | 144 | # elpa packages 145 | /elpa/ 146 | 147 | # reftex files 148 | *.rel 149 | 150 | # AUCTeX auto folder 151 | /auto/ 152 | 153 | # vscode user settings 154 | .vscode 155 | 156 | # cursor 157 | .cursor 158 | 159 | docker_logs 160 | test/e2e/work/* 161 | -------------------------------------------------------------------------------- /test/e2e/aerospike_6.x.conf: -------------------------------------------------------------------------------- 1 | # Aerospike Asadm Test Configuration Template 2 | # 3 | # Copyright (c) 2008-2025 Aerospike, Inc. All rights reserved. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | # this software and associated documentation files (the "Software"), to deal in 7 | # the Software without restriction, including without limitation the rights to 8 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | # of the Software, and to permit persons to whom the Software is furnished to do 10 | # so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | 24 | ${security_stanza} 25 | 26 | service { 27 | cluster-name 6.x-asadm-test 28 | feature-key-file ${feature_path} 29 | run-as-daemon false 30 | work-directory ${state_directory} 31 | pidfile ${state_directory}/asd.pid 32 | proto-fd-max 1024 33 | transaction-retry-ms 10000 34 | transaction-max-ms 10000 35 | } 36 | 37 | logging { 38 | console { 39 | context any info 40 | context security info 41 | } 42 | file ${log_path} { 43 | context any info 44 | } 45 | } 46 | 47 | mod-lua { 48 | user-path ${udf_directory} 49 | } 50 | 51 | network { 52 | service { 53 | port ${service_port} 54 | address any 55 | access-address ${access_address} 56 | } 57 | 58 | heartbeat { 59 | mode mesh 60 | address any 61 | port ${heartbeat_port} 62 | interval 100 63 | timeout 3 64 | connect-timeout-ms 100 65 | ${peer_connection} 66 | } 67 | 68 | fabric { 69 | port ${fabric_port} 70 | address any 71 | } 72 | 73 | info { 74 | port ${info_port} 75 | address any 76 | } 77 | } 78 | 79 | namespace ${namespace} { 80 | replication-factor 2 81 | memory-size 512M 82 | default-ttl 0 83 | storage-engine device { 84 | file /opt/aerospike/data/test.dat 85 | filesize 1G 86 | data-in-memory false # Store data in memory in addition to file. 87 | } 88 | nsup-period 60 89 | } 90 | 91 | xdr { 92 | dc DC1 { 93 | namespace ${namespace} { 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/Makefile: -------------------------------------------------------------------------------- 1 | # Variables required for this Makefile 2 | PKG_DIR = $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 3 | TOP_DIR = $(PKG_DIR)/../build/bin 4 | BUILD_DIR = $(PKG_DIR)/build 5 | TARGET_DIR = $(PKG_DIR)/target 6 | CONFIG_DIR = /etc/aerospike 7 | # Package variables 8 | VERSION = $(shell git describe --tags --always) 9 | RPM_VERSION = $(shell git describe --tags --always | tr '-' '_') 10 | BUILD_DISTRO ?= $(shell .github/packaging/common/os_version.sh) 11 | NAME = "aerospike-$(PACKAGE_NAME)" 12 | MAINTAINER = "Aerospike" 13 | DESCRIPTION = "Asadmin, a tool for managing Aerospike database." 14 | LICENSE = "Apache License 2.0" 15 | URL = "https://github.com/aerospike/$(PACKAGE_NAME)" 16 | VENDOR = "Aerospike, Inc." 17 | ARCH = $(shell uname -m) 18 | 19 | 20 | .PHONY: all 21 | all: deb rpm tar 22 | 23 | .PHONY: deb 24 | deb: prep 25 | fpm --force \ 26 | --config-files $(CONFIG_DIR) \ 27 | --input-type dir \ 28 | --output-type deb \ 29 | --chdir $(BUILD_DIR)/ \ 30 | --name $(NAME) \ 31 | --version $(VERSION) \ 32 | --maintainer $(MAINTAINER) \ 33 | --description $(DESCRIPTION) \ 34 | --license $(LICENSE) \ 35 | --url $(URL) \ 36 | --vendor $(VENDOR) \ 37 | --package $(TARGET_DIR)/$(NAME)_$(VERSION)_$(BUILD_DISTRO)_$(ARCH).deb 38 | 39 | .PHONY: rpm 40 | rpm: prep 41 | fpm --force \ 42 | --config-files $(CONFIG_DIR) \ 43 | --input-type dir \ 44 | --output-type rpm \ 45 | --chdir $(BUILD_DIR)/ \ 46 | --name $(NAME) \ 47 | --version $(RPM_VERSION) \ 48 | --maintainer $(MAINTAINER) \ 49 | --description $(DESCRIPTION) \ 50 | --license $(LICENSE) \ 51 | --url $(URL) \ 52 | --vendor $(VENDOR) \ 53 | --package $(TARGET_DIR)/$(NAME)-$(RPM_VERSION).$(BUILD_DISTRO).$(ARCH).rpm 54 | 55 | .PHONY: tar 56 | tar: prep 57 | fpm --force \ 58 | --config-files $(CONFIG_DIR) \ 59 | --input-type dir \ 60 | --output-type tar \ 61 | --chdir $(BUILD_DIR)/ \ 62 | --name $(NAME) \ 63 | --version $(VERSION) \ 64 | --maintainer $(MAINTAINER) \ 65 | --description $(DESCRIPTION) \ 66 | --license $(LICENSE) \ 67 | --url $(URL) \ 68 | --vendor $(VENDOR) \ 69 | --package $(TARGET_DIR)/$(NAME)-$(BUILD_DISTRO)-$(VERSION)-$(ARCH).tar 70 | 71 | .PHONY: prep 72 | prep: 73 | install -d $(TARGET_DIR) 74 | install -d $(BUILD_DIR)/$(CONFIG_DIR) 75 | install -d $(BUILD_DIR)/opt/aerospike/bin 76 | install -pm 755 $(TOP_DIR)/asadm $(BUILD_DIR)/opt/aerospike/bin 77 | install -pm 755 $(TOP_DIR)/asinfo $(BUILD_DIR)/opt/aerospike/bin 78 | install -d $(BUILD_DIR)/usr/bin 79 | ln -sf /opt/aerospike/bin/asadm $(BUILD_DIR)/usr/bin/asadm 80 | ln -sf /opt/aerospike/bin/asinfo $(BUILD_DIR)/usr/bin/asinfo 81 | 82 | .PHONY: clean 83 | clean: 84 | rm -rf $(TARGET_DIR) 85 | rm -rf $(BUILD_DIR)/opt 86 | rm -rf $(BUILD_DIR)/var -------------------------------------------------------------------------------- /test/e2e/aerospike_latest.conf: -------------------------------------------------------------------------------- 1 | # Aerospike Asadm Test Configuration Template 2 | # 3 | # Copyright (c) 2008-2025 Aerospike, Inc. All rights reserved. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | # this software and associated documentation files (the "Software"), to deal in 7 | # the Software without restriction, including without limitation the rights to 8 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | # of the Software, and to permit persons to whom the Software is furnished to do 10 | # so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | 24 | ${security_stanza} 25 | 26 | service { 27 | cluster-name 7.x-asadm-test 28 | feature-key-file ${feature_path} 29 | run-as-daemon false 30 | work-directory ${state_directory} 31 | pidfile ${state_directory}/asd.pid 32 | proto-fd-max 1024 33 | transaction-retry-ms 10000 34 | transaction-max-ms 10000 35 | } 36 | 37 | logging { 38 | console { 39 | context any info 40 | context security info 41 | } 42 | file ${log_path} { 43 | context any info 44 | } 45 | } 46 | 47 | mod-lua { 48 | user-path ${udf_directory} 49 | } 50 | 51 | network { 52 | service { 53 | port ${service_port} 54 | address any 55 | access-address ${access_address} 56 | } 57 | 58 | heartbeat { 59 | mode mesh 60 | address any 61 | port ${heartbeat_port} 62 | interval 100 63 | timeout 3 64 | connect-timeout-ms 100 65 | ${peer_connection} 66 | } 67 | 68 | fabric { 69 | port ${fabric_port} 70 | address any 71 | } 72 | 73 | info { 74 | port ${info_port} 75 | address any 76 | } 77 | 78 | admin { 79 | port ${admin_port} 80 | address any 81 | } 82 | } 83 | 84 | namespace ${namespace} { 85 | replication-factor 2 86 | default-ttl 0 87 | storage-engine memory { 88 | file /opt/aerospike/data/test.dat 89 | filesize 1G 90 | } 91 | nsup-period 60 92 | } 93 | 94 | namespace ${namespace}_sc { 95 | strong-consistency true 96 | replication-factor 1 97 | default-ttl 0 98 | storage-engine memory { 99 | file /opt/aerospike/data/test_sc.dat 100 | filesize 1G 101 | } 102 | nsup-period 60 103 | } 104 | 105 | xdr { 106 | dc DC1 { 107 | namespace ${namespace} { 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/collectinfo_root_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from lib.base_controller import BaseController, CommandHelp, ShellException 16 | 17 | from .collectinfo_handler.log_handler import CollectinfoLogHandler, LogHandlerException 18 | from .collectinfo_command_controller import CollectinfoCommandController 19 | from .features_controller import FeaturesController 20 | from .health_check_controller import HealthCheckController 21 | from .info_controller import InfoController 22 | from .list_controller import ListController 23 | from .page_controller import PagerController 24 | from .show_controller import ShowController 25 | from .summary_controller import SummaryController 26 | 27 | 28 | @CommandHelp("Aerospike Admin") 29 | class CollectinfoRootController(BaseController): 30 | log_handler = None 31 | command = None 32 | 33 | def __init__(self, asadm_version="", clinfo_path=""): 34 | BaseController.asadm_version = asadm_version 35 | 36 | # Create Static Instance of Loghdlr 37 | try: 38 | CollectinfoRootController.log_handler = CollectinfoLogHandler(clinfo_path) 39 | except LogHandlerException as e: 40 | raise ShellException(e) 41 | 42 | CollectinfoRootController.command = CollectinfoCommandController( 43 | self.log_handler 44 | ) 45 | 46 | self.controller_map = { 47 | "list": ListController, 48 | "show": ShowController, 49 | "info": InfoController, 50 | "features": FeaturesController, 51 | "pager": PagerController, 52 | "health": HealthCheckController, 53 | "summary": SummaryController, 54 | } 55 | 56 | def close(self): 57 | try: 58 | self.log_handler.close() 59 | except Exception: 60 | pass 61 | 62 | @CommandHelp("Terminate session") 63 | def do_exit(self, line): 64 | # This function is a hack for autocomplete 65 | return "EXIT" 66 | 67 | @CommandHelp( 68 | "Displays the documentation for the specified command.", 69 | "For example, to see the documentation for the 'info' command,", 70 | "use the command 'help info'.", 71 | short_msg="Displays the documentation for the specified command", 72 | hide=True, 73 | ) 74 | def do_help(self, line): 75 | self.view.print_result(self.execute_help(line)) 76 | -------------------------------------------------------------------------------- /lib/utils/file_size.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | byte = [ 16 | (1024.0**5, " PB"), 17 | (1024.0**4, " TB"), 18 | (1024.0**3, " GB"), 19 | (1024.0**2, " MB"), 20 | (1024.0**1, " KB"), 21 | (1024.0**0, " B "), 22 | ] 23 | 24 | byte_verbose = [ 25 | (1024**5, (" petabyte ", " petabytes")), 26 | (1024**4, (" terabyte ", " terabytes")), 27 | (1024**3, (" gigabyte ", " gigabytes")), 28 | (1024**2, (" megabyte ", " megabytes")), 29 | (1024**1, (" kilobyte ", " kilobytes")), 30 | (1024**0, (" byte ", " bytes ")), 31 | ] 32 | 33 | si = [ 34 | (1000**5, " P"), 35 | (1000**4, " T"), 36 | (1000**3, " G"), 37 | (1000**2, " M"), 38 | (1000**1, " K"), 39 | (1000**0, " "), 40 | ] 41 | 42 | si_float = [ 43 | (1000.0**5, " P"), 44 | (1000.0**4, " T"), 45 | (1000.0**3, " G"), 46 | (1000.0**2, " M"), 47 | (1000.0**1, " K"), 48 | (1000.0**0, " "), 49 | ] 50 | 51 | time = [ 52 | ((60.0**2) * 24, " days"), 53 | (60.0**2, " hrs "), 54 | (60.0**1, " mins"), 55 | (60.0**0, " secs"), 56 | ] 57 | 58 | systems = (byte, byte_verbose, si, si_float, time) 59 | 60 | 61 | def size(bytes, system=byte): 62 | """ 63 | Human-readable file size. 64 | """ 65 | for factor, suffix in system: 66 | if bytes >= factor: 67 | break 68 | amount = bytes / factor 69 | if isinstance(suffix, tuple): 70 | singular, multiple = suffix 71 | if amount == 1: 72 | suffix = singular 73 | else: 74 | suffix = multiple 75 | if type(amount) == float: 76 | return "%0.3f%s" % (amount, suffix) 77 | else: 78 | return str(amount) + suffix 79 | 80 | 81 | def is_file_size(value): 82 | global systems 83 | try: 84 | float(str(value)) 85 | return True 86 | except ValueError: 87 | pass # continue 88 | 89 | def isnumeric_helper(suffix): 90 | tmp_value = value.replace(suffix, "") 91 | tmp_value.strip() 92 | try: 93 | float(tmp_value) 94 | return True 95 | except ValueError: 96 | return False 97 | 98 | for system in systems: 99 | for factor, suffix in system: 100 | if type(suffix) is str: 101 | if isnumeric_helper(suffix): 102 | return True 103 | else: 104 | for name in suffix: 105 | if isnumeric_helper(name): 106 | return True 107 | return False 108 | -------------------------------------------------------------------------------- /lib/live_cluster/asinfo_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import logging 15 | from lib.utils import constants 16 | from lib.base_controller import CommandHelp, ModifierHelp, ShellException 17 | 18 | from .live_cluster_command_controller import LiveClusterCommandController 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | @CommandHelp( 24 | "Provides raw access to the info protocol.", 25 | usage=f"[-v ] [-l] [--no_node_name] [{constants.Modifiers.LIKE} ] [{constants.ModifierUsage.WITH}]", 26 | modifiers=( 27 | ModifierHelp( 28 | "-v", 29 | 'The command to execute, e.g. "get-stats:context=xdr;dc=dataCenterName"', 30 | ), 31 | ModifierHelp( 32 | "-l", 33 | 'Replace semicolons ";" with newlines. If output does not contain semicolons "-l" will attempt to use colons ":" followed by commas ",".', 34 | ), 35 | ModifierHelp( 36 | "--no_node_name", "Force to display output without printing node names." 37 | ), 38 | ModifierHelp( 39 | constants.Modifiers.LIKE, "Filter returned fields by substring match" 40 | ), 41 | ModifierHelp(constants.Modifiers.WITH, constants.ModifierHelpText.WITH), 42 | ), 43 | ) 44 | class ASInfoController(LiveClusterCommandController): 45 | def __init__(self): 46 | self.modifiers = set(["with", "like"]) 47 | 48 | @CommandHelp("Executes an info command.") 49 | async def _do_default(self, line): 50 | mods = self.parse_modifiers(line) 51 | line = mods["line"] 52 | nodes = self.nodes 53 | 54 | value = None 55 | line_sep = False 56 | show_node_name = True 57 | 58 | tline = line[:] 59 | 60 | try: 61 | while tline: 62 | word = tline.pop(0) 63 | if word == "-v": 64 | value = tline.pop(0) 65 | elif word == "-l": 66 | line_sep = True 67 | elif word == "--no_node_name": 68 | show_node_name = False 69 | else: 70 | raise ShellException( 71 | "Do not understand '%s' in '%s'" % (word, " ".join(line)) 72 | ) 73 | except Exception: 74 | logger.warning("Do not understand '%s' in '%s'" % (word, " ".join(line))) 75 | return 76 | if value is not None: 77 | value = value.translate(str.maketrans("", "", "'\"")) 78 | 79 | results = await self.cluster.info(value, nodes=nodes) 80 | 81 | return self.view.asinfo(results, line_sep, show_node_name, self.cluster, **mods) 82 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # abacaca avva 12 | # 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ "master" ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ "master" ] 21 | schedule: 22 | - cron: '21 5 * * 3' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'python' ] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 38 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 39 | 40 | steps: 41 | - name: Harden the runner (Audit all outbound calls) 42 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 43 | with: 44 | egress-policy: audit 45 | 46 | - name: Checkout repository 47 | uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 48 | 49 | # Initializes the CodeQL tools for scanning. 50 | - name: Initialize CodeQL 51 | uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 52 | with: 53 | languages: ${{ matrix.language }} 54 | # If you wish to specify custom queries, you can do so here or in a config file. 55 | # By default, queries listed here will override any specified in a config file. 56 | # Prefix the list here with "+" to use these queries and those in the config file. 57 | 58 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 59 | # queries: security-extended,security-and-quality 60 | 61 | 62 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 63 | # If this step fails, then you should remove it and run the build manually (see below) 64 | - name: Autobuild 65 | uses: github/codeql-action/autobuild@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 66 | 67 | # ℹ️ Command-line programs to run using the OS shell. 68 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 69 | 70 | # If the Autobuild fails above, remove it and uncomment the following three lines. 71 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 72 | 73 | # - run: | 74 | # echo "Run, Build Application using script" 75 | # ./location_of_script_within_repo/buildscript.sh 76 | 77 | - name: Perform CodeQL Analysis 78 | uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 79 | -------------------------------------------------------------------------------- /lib/view/terminal/get_terminal_size.py: -------------------------------------------------------------------------------- 1 | """This is a backport of shutil.get_terminal_size from Python 3.3. 2 | 3 | The original implementation is in C, but here we use the ctypes and 4 | fcntl modules to create a pure Python version of os.get_terminal_size. 5 | """ 6 | 7 | import os 8 | import struct 9 | import sys 10 | 11 | from collections import namedtuple 12 | 13 | __all__ = ["get_terminal_size"] 14 | 15 | 16 | terminal_size = namedtuple("terminal_size", "columns lines") 17 | 18 | try: 19 | from ctypes import windll, create_string_buffer, WinError 20 | 21 | _handle_ids = { 22 | 0: -10, 23 | 1: -11, 24 | 2: -12, 25 | } 26 | 27 | def _get_terminal_size(fd): 28 | handle = windll.kernel32.GetStdHandle(_handle_ids[fd]) 29 | if handle == 0: 30 | raise OSError("handle cannot be retrieved") 31 | if handle == -1: 32 | raise WinError() 33 | csbi = create_string_buffer(22) 34 | res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi) 35 | if res: 36 | res = struct.unpack("hhhhHhhhhhh", csbi.raw) 37 | left, top, right, bottom = res[5:9] 38 | columns = right - left + 1 39 | lines = bottom - top + 1 40 | return terminal_size(columns, lines) 41 | else: 42 | raise WinError() 43 | 44 | except ImportError: 45 | import fcntl 46 | import termios 47 | 48 | def _get_terminal_size(fd): 49 | try: 50 | res = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\x00" * 4) 51 | except IOError as e: 52 | raise OSError(e) 53 | lines, columns = struct.unpack("hh", res) 54 | 55 | return terminal_size(columns, lines) 56 | 57 | 58 | def get_terminal_size(fallback=(160, 48)): 59 | """Get the size of the terminal window. 60 | 61 | For each of the two dimensions, the environment variable, COLUMNS 62 | and LINES respectively, is checked. If the variable is defined and 63 | the value is a positive integer, it is used. 64 | 65 | When COLUMNS or LINES is not defined, which is the common case, 66 | the terminal connected to sys.__stdout__ is queried 67 | by invoking os.get_terminal_size. 68 | 69 | If the terminal size cannot be successfully queried, either because 70 | the system doesn't support querying, or because we are not 71 | connected to a terminal, the value given in fallback parameter 72 | is used. Fallback defaults to (80, 24) which is the default 73 | size used by many terminal emulators. 74 | 75 | The value returned is a named tuple of type os.terminal_size. 76 | """ 77 | # Try the environment first 78 | try: 79 | columns = int(os.environ["COLUMNS"]) 80 | except (KeyError, ValueError): 81 | columns = 0 82 | 83 | try: 84 | lines = int(os.environ["LINES"]) 85 | except (KeyError, ValueError): 86 | lines = 0 87 | 88 | # Only query if necessary 89 | if columns <= 0 or lines <= 0: 90 | try: 91 | size = _get_terminal_size(sys.__stdout__.fileno()) 92 | except (NameError, OSError): 93 | size = terminal_size(*fallback) 94 | 95 | if columns <= 0: 96 | columns = size.columns 97 | if lines <= 0: 98 | lines = size.lines 99 | 100 | return terminal_size(columns, lines) 101 | -------------------------------------------------------------------------------- /lib/collectinfo_analyzer/features_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from lib.collectinfo_analyzer.collectinfo_command_controller import ( 16 | CollectinfoCommandController, 17 | ) 18 | from lib.base_controller import CommandHelp, ModifierHelp 19 | from lib.utils import constants, common 20 | 21 | 22 | @CommandHelp( 23 | "Lists the features in use in a running Aerospike cluster.", 24 | usage=f"[{constants.Modifiers.LIKE} ]", 25 | modifiers=( 26 | ModifierHelp(constants.Modifiers.LIKE, "Filter features by substring match"), 27 | ), 28 | ) 29 | class FeaturesController(CollectinfoCommandController): 30 | def __init__(self): 31 | self.modifiers = set(["like"]) 32 | 33 | def _do_default(self, line): 34 | service_stats = self.log_handler.info_statistics(stanza=constants.STAT_SERVICE) 35 | namespace_stats = self.log_handler.info_statistics( 36 | stanza=constants.STAT_NAMESPACE 37 | ) 38 | xdr_dc_stats = self.log_handler.info_statistics(stanza=constants.STAT_XDR) 39 | service_configs = self.log_handler.info_getconfig( 40 | stanza=constants.CONFIG_SERVICE 41 | ) 42 | namespace_configs = self.log_handler.info_getconfig( 43 | stanza=constants.CONFIG_NAMESPACE 44 | ) 45 | security_configs = self.log_handler.info_getconfig( 46 | stanza=constants.CONFIG_SECURITY 47 | ) 48 | 49 | for timestamp in sorted(service_stats.keys()): 50 | features = {} 51 | s_stats = service_stats[timestamp] 52 | ns_stats = {} 53 | dc_stats = {} 54 | s_configs = {} 55 | ns_configs = {} 56 | sec_configs = {} 57 | 58 | if timestamp in service_configs: 59 | s_configs = service_configs[timestamp] 60 | 61 | if timestamp in namespace_stats: 62 | ns_stats = namespace_stats[timestamp] 63 | 64 | if timestamp in xdr_dc_stats: 65 | dc_stats = xdr_dc_stats[timestamp] 66 | 67 | if timestamp in namespace_configs: 68 | ns_configs = namespace_configs[timestamp] 69 | 70 | if timestamp in security_configs: 71 | sec_configs = security_configs[timestamp] 72 | 73 | features = common.find_nodewise_features( 74 | service_stats=s_stats, 75 | ns_stats=ns_stats, 76 | xdr_dc_stats=dc_stats, 77 | service_configs=s_configs, 78 | ns_configs=ns_configs, 79 | security_configs=sec_configs, 80 | ) 81 | 82 | self.view.show_config( 83 | "Features", 84 | features, 85 | self.log_handler.get_cinfo_log_at(timestamp=timestamp), 86 | timestamp=timestamp, 87 | **self.mods, 88 | ) 89 | -------------------------------------------------------------------------------- /lib/view/sheet/render/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from collections import defaultdict 16 | import logging 17 | from typing import Any 18 | 19 | from lib.view.sheet.decleration import Sheet 20 | 21 | from ..const import SheetStyle 22 | from .column_rsheet import ColumnRSheet 23 | from .row_rsheet import RowRSheet 24 | from .json_rsheet import JSONRSheet 25 | 26 | 27 | render_class = { 28 | SheetStyle.columns: ColumnRSheet, 29 | SheetStyle.rows: RowRSheet, 30 | SheetStyle.json: JSONRSheet, 31 | } 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | use_json = False 36 | 37 | 38 | def set_style_json(value=True): 39 | global use_json 40 | use_json = value 41 | 42 | 43 | def get_style_json(): 44 | global use_json 45 | return use_json 46 | 47 | 48 | def render( 49 | sheet: Sheet, 50 | title: str, 51 | data_source: dict[str, Any], 52 | style=None, 53 | common=None, 54 | description=None, 55 | selectors=None, 56 | title_repeat=False, 57 | disable_aggregations=False, 58 | dynamic_diff=False, 59 | ): 60 | """ 61 | Arguments: 62 | sheet -- The decleration.sheet to render. 63 | title -- Title for this render. 64 | data_source -- Dictionary of data_sources to project fields from. 65 | 66 | Keyword Arguments: 67 | style -- 'SheetStyle.columns': Output fields as columns. 68 | 'SheetStyle.rows' : Output fields as rows. 69 | 'SheetStyle.json' : Output sheet as JSON. 70 | common -- A dict of common information passed to each entry. 71 | description -- A description of the sheet. 72 | selectors -- List of regular expressions to select which fields from 73 | dynamic fields. 74 | title_repeat -- Repeat title and row headers every n columns. TODO: Add for column style tables 75 | disable_aggregations -- Disable sheet aggregations. 76 | dynamic_diff -- Only show dynamic fields that aren't uniform. 77 | """ 78 | tcommon = defaultdict(lambda: None) 79 | 80 | if common is not None: 81 | tcommon.update(common) 82 | 83 | assert ( 84 | set(sheet.from_sources) - set(data_source.keys()) == set() 85 | ), "Mismatched sheet sources and data sources. Sheet: {}, Data: {}".format( 86 | set(sheet.from_sources), set(data_source.keys()) 87 | ) 88 | 89 | if use_json: 90 | style = SheetStyle.json 91 | elif style is None: 92 | style = sheet.default_style 93 | 94 | return render_class[style]( 95 | sheet, 96 | title, 97 | data_source, 98 | tcommon, 99 | description=description, 100 | selectors=selectors, 101 | title_repeat=title_repeat, 102 | disable_aggregations=disable_aggregations, 103 | dynamic_diff=dynamic_diff, 104 | ).render() 105 | -------------------------------------------------------------------------------- /test/unit/view/test_templates.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | from parameterized import parameterized 17 | 18 | from lib.view import templates 19 | from lib.view.sheet.decleration import EntryData 20 | 21 | 22 | class HelperTests(unittest.TestCase): 23 | @parameterized.expand( 24 | [ 25 | ([0.5, 0.8, 0.3], [10, 15, 20], 0.511), 26 | ([0.5, 0.8, 0.3], [-1, 0, 1], 0), 27 | ([4.0, 10.59, 8.35], [65701.6, 64926.1, 65567.2], 7.635), 28 | ], 29 | ) 30 | def test_weighted_avg(self, values, weights, expected): 31 | self.assertEqual(round(templates.weighted_avg(values, weights), 3), expected) 32 | 33 | @parameterized.expand( 34 | [ 35 | ( 36 | [ 37 | EntryData(0.5, None, {"ops/sec": 10}, None, False, False), 38 | EntryData(0.8, None, {"ops/sec": 15}, None, False, False), 39 | EntryData(0.3, None, {"ops/sec": 20}, None, False, False), 40 | ], 41 | 0.511, 42 | ), 43 | ], 44 | ) 45 | def test_latency_weighted_avg(self, edatas, expected): 46 | self.assertEqual(round(templates.latency_weighted_avg(edatas), 3), expected) 47 | 48 | @parameterized.expand( 49 | [ 50 | ( 51 | [ 52 | EntryData(0.5, None, {"type": {"Total": 10}}, None, False, False), 53 | EntryData(0.8, None, {"type": {"Total": 15}}, None, False, False), 54 | EntryData(0.3, None, {"type": {"Total": 20}}, None, False, False), 55 | ], 56 | 0.511, 57 | ), 58 | ], 59 | ) 60 | def test_create_usage_weighted_avg(self, edatas, expected): 61 | func = templates.create_usage_weighted_avg("type") 62 | self.assertEqual(round(func(edatas), 3), expected) 63 | 64 | @parameterized.expand( 65 | [ 66 | # Test compression enabled with non-zero value 67 | ({"latest": 1000000}, True, "(976.562 KB) ?"), 68 | # Test compression disabled with non-zero value 69 | ({"latest": 1000000}, False, "976.562 KB"), 70 | # Test compression enabled with zero value (no parentheses) 71 | ({"latest": 0}, True, "0.000 B"), 72 | # Test compression disabled with zero value 73 | ({"latest": 0}, False, "0.000 B"), 74 | # Test empty license data 75 | ({}, True, "0.000 B"), 76 | # Test None license data 77 | (None, True, "0.000 B"), 78 | ] 79 | ) 80 | def test_format_license_latest_with_compression( 81 | self, license_data, compression_enabled, expected 82 | ): 83 | """Test format_license_latest_with_compression function""" 84 | result = templates.format_license_latest_with_compression( 85 | license_data, compression_enabled 86 | ) 87 | self.assertEqual(result, expected) 88 | -------------------------------------------------------------------------------- /lib/utils/data.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __author__ = "aerospike" 16 | 17 | lsof_file_type_desc = { 18 | "GDIR": "GDIR", 19 | "GREG": "GREG", 20 | "VDIR": "VDIR", 21 | "IPv4": "IPv4 socket", 22 | "IPv6": "IPv6 network file", 23 | "ax25": "Linux AX.25 socket", 24 | "inet": "Internet domain socket", 25 | "lla": "HP-UX link level access file", 26 | "rte": "AF_ROUTE socket", 27 | "sock": "socket of unknown domain", 28 | "unix": "UNIX domain socket", 29 | "x.25": "HP-UX x.25 socket", 30 | "BLK": "block special file", 31 | "CHR": "character special file", 32 | "DEL": "Deleted Linux map file", 33 | "DIR": "directory", 34 | "DOOR": "VDOOR file", 35 | "FIFO": "FIFO special file", 36 | "KQUEUE": "BSD style kernel event queue file", 37 | "LINK": "symbolic link file", 38 | "MPB": "multiplexed block file", 39 | "MPC": "multiplexed character file", 40 | "NOFD": "Linux /proc//fd directory that can't be opened", 41 | "PAS": "/proc/as file", 42 | "PAXV": "/proc/auxv file", 43 | "PCRE": "/proc/cred file", 44 | "PCTL": "/proc control file", 45 | "PCUR": "current /proc process", 46 | "PCWD": "/proc current working directory", 47 | "PDIR": "/proc directory", 48 | "PETY": "/proc executable type (etype)", 49 | "PFD": "/proc file descriptor", 50 | "PFDR": "/proc file descriptor directory", 51 | "PFIL": "executable /proc file", 52 | "PFPR": "/proc FP register set", 53 | "PGD": "/proc/pagedata file", 54 | "PGID": "/proc group notifier file", 55 | "PIPE": "pipes", 56 | "PLC": "/proc/lwpctl file", 57 | "PLDR": "/proc/lpw directory", 58 | "PLDT": "/proc/ldt file", 59 | "PLPI": "/proc/lpsinfo file", 60 | "PLST": "/proc/lstatus file", 61 | "PLU": "/proc/lusage file", 62 | "PLWG": "/proc/gwindows file", 63 | "PLWI": "/proc/lwpsinfo file", 64 | "PLWS": "/proc/lwpstatus file", 65 | "PLWU": "/proc/lwpusage file", 66 | "PLWX": "/proc/xregs file", 67 | "PMAP": "/proc map file (map)", 68 | "PMEM": "/proc memory image file", 69 | "PNTF": "/proc process notifier file", 70 | "POBJ": "/proc/object file", 71 | "PODR": "/proc/object directory", 72 | "POLP": "old format /proc light weight process file", 73 | "POPF": "old format /proc PID file", 74 | "POPG": "old format /proc page data file", 75 | "PORT": "SYSV named pipe", 76 | "PREG": "/proc register file", 77 | "PRMP": "/proc/rmap file", 78 | "PRTD": "/proc root directory", 79 | "PSGA": "/proc/sigact file", 80 | "PSIN": "/proc/psinfo file", 81 | "PSTA": "/proc status file", 82 | "PSXSEM": "POSIX semaphore file", 83 | "PSXSHM": "POSIX shared memory file", 84 | "PUSG": "/proc/usage file", 85 | "PW": "/proc/watch file", 86 | "PXMP": "/proc/xmap file", 87 | "REG": "regular file", 88 | "SMT": "shared memory transport file", 89 | "STSO": "stream socket", 90 | "UNNM": "unnamed type file", 91 | "XNAM": "OpenServer Xenix special file of unknown type", 92 | "XSEM": "OpenServer Xenix semaphore file", 93 | "XSD": "OpenServer Xenix shared data file", 94 | } 95 | -------------------------------------------------------------------------------- /test/unit/live_cluster/client/test_msgpack.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | from lib.live_cluster.client.ctx import ASValues, CDTContext, CTXItems 17 | 18 | from lib.live_cluster.client.msgpack import ( 19 | AS_BYTES_BLOB, 20 | AS_BYTES_STRING, 21 | ASPacker, 22 | CTXItemWireType, 23 | ) 24 | 25 | 26 | class MsgPackTest(unittest.TestCase): 27 | def setUp(self): 28 | self.packer = ASPacker() 29 | 30 | def pack(self, val) -> bytes: 31 | self.packer.pack(val) 32 | return self.packer.bytes() 33 | 34 | def test_pack_as_string_fixstr(self): 35 | string = "abcd" 36 | as_string = ASValues.ASString(string) 37 | expected = bytes([0xA5, AS_BYTES_STRING]) + bytes(string, encoding="utf-8") 38 | 39 | actual = self.pack(as_string) 40 | 41 | self.assertEqual(expected, actual) 42 | 43 | def test_pack_as_string_8(self): 44 | string = "abcdefghijklmnopqrstuvwxyz123456" 45 | as_string = ASValues.ASString(string) 46 | expected = bytearray([0xD9, 33, AS_BYTES_STRING]) 47 | expected.extend(bytearray(string, encoding="ascii")) 48 | 49 | actual = self.pack(as_string) 50 | 51 | self.assertEqual(expected, actual) 52 | 53 | def test_pack_as_bytes_fixstr(self): 54 | string = b"abcd" 55 | as_string = ASValues.ASBytes(string) 56 | expected = bytearray([0xA5, AS_BYTES_BLOB]) 57 | expected.extend(bytearray(string)) 58 | 59 | actual = self.pack(as_string) 60 | 61 | self.assertEqual(expected, actual) 62 | 63 | def test_pack_as_bool_false(self): 64 | actual = self.pack(ASValues.ASBool(False)) 65 | self.assertEqual(bytes([0xC2]), actual) 66 | 67 | def test_pack_as_bool_true(self): 68 | actual = self.pack(ASValues.ASBool(True)) 69 | self.assertEqual(bytes([0xC3]), actual) 70 | 71 | def test_pack_cdt_ctx(self): 72 | ctx = CDTContext( 73 | [ 74 | CTXItems.ListIndex(1), 75 | CTXItems.ListRank(2), 76 | CTXItems.MapIndex(3), 77 | CTXItems.MapRank(4), 78 | CTXItems.MapKey(ASValues.ASString("abcd")), 79 | CTXItems.ListValue(ASValues.ASBool(True)), 80 | CTXItems.MapValue(ASValues.ASInt(5)), 81 | ] 82 | ) 83 | 84 | expected = ( 85 | bytes([0x9E]) 86 | + bytes([CTXItemWireType.AS_CDT_CTX_LIST_INDEX, 1]) 87 | + bytes([CTXItemWireType.AS_CDT_CTX_LIST_RANK, 2]) 88 | + bytes([CTXItemWireType.AS_CDT_CTX_MAP_INDEX, 3]) 89 | + bytes([CTXItemWireType.AS_CDT_CTX_MAP_RANK, 4]) 90 | + bytes([CTXItemWireType.AS_CDT_CTX_MAP_KEY]) 91 | + bytes([0xA5, AS_BYTES_STRING]) 92 | + bytes("abcd", encoding="utf-8") 93 | + bytes([CTXItemWireType.AS_CDT_CTX_LIST_VALUE, 0xC3]) 94 | + bytes([CTXItemWireType.AS_CDT_CTX_MAP_VALUE, 5]) 95 | ) 96 | 97 | actual = self.pack(ctx) 98 | 99 | self.assertEqual(expected, actual) 100 | -------------------------------------------------------------------------------- /lib/base_get_controller.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from lib.utils import constants 4 | 5 | 6 | class BaseGetConfigController: 7 | def get_logging( 8 | self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL 9 | ): 10 | raise NotImplementedError("get_logging not implemented") 11 | 12 | def get_service( 13 | self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL 14 | ): 15 | raise NotImplementedError("get_service not implemented") 16 | 17 | def get_network( 18 | self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL 19 | ): 20 | raise NotImplementedError("get_network not implemented") 21 | 22 | def get_security( 23 | self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL 24 | ): 25 | raise NotImplementedError("get_security not implemented") 26 | 27 | def get_namespace( 28 | self, 29 | for_mods: list[str] | None = None, 30 | nodes: constants.NodeSelectionType = constants.NodeSelection.ALL, 31 | ): 32 | raise NotImplementedError("get_namespace not implemented") 33 | 34 | def get_sets( 35 | self, 36 | for_mods: list[str] | None = None, 37 | flip=False, 38 | nodes: constants.NodeSelectionType = constants.NodeSelection.ALL, 39 | ): 40 | raise NotImplementedError("get_sets not implemented") 41 | 42 | def get_rack_ids( 43 | self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL 44 | ): 45 | raise NotImplementedError("get_rack_ids not implemented") 46 | 47 | def get_xdr(self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL): 48 | raise NotImplementedError("get_xdr not implemented") 49 | 50 | def get_xdr_dcs( 51 | self, 52 | flip=False, 53 | for_mods: list[str] | None = None, 54 | nodes: constants.NodeSelectionType = constants.NodeSelection.ALL, 55 | ): 56 | raise NotImplementedError("get_xdr_dcs not implemented") 57 | 58 | def get_xdr_namespaces( 59 | self, 60 | for_mods: list[str] | None = None, 61 | nodes: constants.NodeSelectionType = constants.NodeSelection.ALL, 62 | ): 63 | raise NotImplementedError("get_xdr_namespaces not implemented") 64 | 65 | def get_xdr_filters( 66 | self, 67 | for_mods: list[str] | None = None, 68 | nodes: constants.NodeSelectionType = constants.NodeSelection.PRINCIPAL, 69 | ): 70 | raise NotImplementedError("get_xdr_filters not implemented") 71 | 72 | 73 | # class BaseGetStatisticsController: 74 | # def get_service(self): 75 | # pass 76 | 77 | # def get_namespace(self, for_mods=None): 78 | # pass 79 | 80 | # def get_sets(self, for_mods=None, flip=False): 81 | # pass 82 | 83 | # def get_sindex(self): 84 | # pass 85 | 86 | # # TODO might be a good place to add support for the with modifier to filter nodes 87 | 88 | # def get_xdr( 89 | # self, 90 | # ) -> dict[str, Any]: # type: ignore 91 | # pass 92 | 93 | # def get_xdr_dcs(self, for_mods: list[str] | None = None): 94 | # pass 95 | 96 | # def get_xdr_namespaces(self, for_mods=None): 97 | # pass 98 | 99 | 100 | # class GetAclController: 101 | # def get_users( 102 | # self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL 103 | # ): 104 | # pass 105 | 106 | # def get_user( 107 | # self, 108 | # username: str, 109 | # nodes: constants.NodeSelectionType = constants.NodeSelection.ALL, 110 | # ): 111 | # pass 112 | 113 | # def get_roles( 114 | # self, nodes: constants.NodeSelectionType = constants.NodeSelection.ALL 115 | # ): 116 | # pass 117 | 118 | # def get_role( 119 | # self, 120 | # role_name, 121 | # nodes: constants.NodeSelectionType = constants.NodeSelection.ALL, 122 | # ): 123 | # pass 124 | -------------------------------------------------------------------------------- /lib/live_cluster/client/ctx.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Generic, TypeVar 16 | 17 | T = TypeVar("T") 18 | 19 | 20 | class ASValue(Generic[T]): 21 | def __init__(self, value: T): 22 | self.value = value 23 | 24 | def __eq__(self, __o: object) -> bool: 25 | return type(self) is type(__o) and self.__dict__ == __o.__dict__ 26 | 27 | def __str__(self) -> str: 28 | return "{}({})".format(type(self), self.value) 29 | 30 | 31 | class ASValues: 32 | class ASBool(ASValue[bool]): 33 | def __init__(self, value: bool): 34 | super().__init__(value) 35 | 36 | class ASInt(ASValue[int]): 37 | def __init__(self, value: int): 38 | super().__init__(value) 39 | 40 | class ASString(ASValue[str]): 41 | def __init__(self, value: str): 42 | super().__init__(value) 43 | 44 | class ASBytes(ASValue[bytes]): 45 | def __init__(self, value: bytes): 46 | super().__init__(value) 47 | 48 | class ASDouble(ASValue[float]): 49 | def __init__(self, value: float): 50 | super().__init__(value) 51 | 52 | """ 53 | Not used. Here for reference in case they become needed. 54 | """ 55 | 56 | # class ASList(ASValue[list[ASValue]]): 57 | # def __init__(self, value: list[ASValue]): 58 | # super().__init__(value) 59 | 60 | # class ASMap(ASValue[dict[ASValue, ASValue]]): 61 | # def __init__(self, value: dict[ASValue, ASValue]): 62 | # super().__init__(value) 63 | 64 | # class ASGeoJson(ASValue[str]): 65 | # def __init__(self, value: str): 66 | # super().__init__(value) 67 | 68 | # class ASWildCard(ASValue[None]): 69 | # def __init__(self): # maybe could be dict? 70 | # super().__init__(None) 71 | 72 | # class ASUndef(ASValue): 73 | # def __init__(self): 74 | # super().__init__(None) 75 | 76 | # class ASNil(ASValue): 77 | # def __init__(self): 78 | # super().__init__(None) 79 | 80 | # class ASRec(ASValue): 81 | # def __init__(self, value): 82 | # super().__init__(value) 83 | 84 | 85 | class CTXItem(Generic[T]): 86 | def __init__(self, val: T): 87 | self.value = val 88 | 89 | def __eq__(self, __o: object) -> bool: 90 | return type(__o) is type(self) and __o.__dict__ == self.__dict__ 91 | 92 | def __str__(self): 93 | return "{}({})".format(type(self), self.value) 94 | 95 | 96 | class CTXIntItem(CTXItem[int]): 97 | def __init__(self, val: int): 98 | if not isinstance(val, int): 99 | raise TypeError("CTX value must of type int") 100 | 101 | super().__init__(val) 102 | 103 | 104 | class CTXParticleItem(CTXItem[ASValue]): 105 | def __init__(self, val: ASValue): 106 | if not isinstance(val, ASValue): 107 | raise TypeError("CTX value must of type ASValue") 108 | 109 | super().__init__(val) 110 | 111 | 112 | class CTXItems: 113 | class ListIndex(CTXIntItem): 114 | pass 115 | 116 | class ListRank(CTXIntItem): 117 | pass 118 | 119 | class ListValue(CTXParticleItem): 120 | pass 121 | 122 | class MapIndex(CTXIntItem): 123 | pass 124 | 125 | class MapRank(CTXIntItem): 126 | pass 127 | 128 | class MapKey(CTXParticleItem): 129 | pass 130 | 131 | class MapValue(CTXParticleItem): 132 | pass 133 | 134 | 135 | class CDTContext(list[CTXItem]): 136 | pass 137 | -------------------------------------------------------------------------------- /test/unit/collectinfo_analyzer/test_log_handler.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import shutil 17 | import tempfile 18 | import unittest 19 | 20 | from mock import patch 21 | 22 | from lib.collectinfo_analyzer.collectinfo_handler.log_handler import ( 23 | CollectinfoLogHandler, 24 | ) 25 | from lib.utils import log_util 26 | 27 | 28 | class LogUtilTest(unittest.TestCase): 29 | def setUp(self): 30 | self.temp_dir = tempfile.mkdtemp() 31 | 32 | def tearDown(self): 33 | shutil.rmtree(self.temp_dir) 34 | 35 | @patch("platform.system") 36 | def test_get_all_files_on_darwin(self, mock_system): 37 | # Simulate macOS platform 38 | mock_system.return_value = "Darwin" 39 | 40 | # Create test files 41 | regular_file = os.path.join(self.temp_dir, "test.log") 42 | resource_fork_file = os.path.join(self.temp_dir, "._test.log") 43 | 44 | with open(regular_file, "w") as f: 45 | f.write("log content") 46 | with open(resource_fork_file, "w") as f: 47 | f.write("resource fork content") 48 | 49 | # Test get_all_files method 50 | files = log_util.get_all_files(self.temp_dir) 51 | 52 | # Assert resource fork file is excluded on Darwin 53 | self.assertIn("test.log", [os.path.basename(f) for f in files]) 54 | self.assertNotIn("._test.log", [os.path.basename(f) for f in files]) 55 | 56 | @patch("platform.system") 57 | def test_get_all_files_on_linux(self, mock_system): 58 | # Simulate Linux platform 59 | mock_system.return_value = "Linux" 60 | 61 | # Create test files 62 | regular_file = os.path.join(self.temp_dir, "test.log") 63 | resource_fork_file = os.path.join(self.temp_dir, "._test.log") 64 | 65 | with open(regular_file, "w") as f: 66 | f.write("log content") 67 | with open(resource_fork_file, "w") as f: 68 | f.write("resource fork content") 69 | 70 | # Test get_all_files method 71 | files = log_util.get_all_files(self.temp_dir) 72 | 73 | # Assert both files are included on Linux 74 | self.assertIn("test.log", [os.path.basename(f) for f in files]) 75 | self.assertIn("._test.log", [os.path.basename(f) for f in files]) 76 | 77 | 78 | class CollectinfoLogHandlerTest(unittest.TestCase): 79 | def test_info_masking_rules_method_exists(self): 80 | """Test that info_masking_rules method exists""" 81 | # Just test that the method exists and is callable 82 | self.assertTrue(hasattr(CollectinfoLogHandler, "info_masking_rules")) 83 | self.assertTrue(callable(getattr(CollectinfoLogHandler, "info_masking_rules"))) 84 | 85 | @patch( 86 | "lib.collectinfo_analyzer.collectinfo_handler.log_handler.CollectinfoLogHandler._fetch_from_cinfo_log" 87 | ) 88 | def test_info_masking_rules_calls_fetch(self, fetch_mock): 89 | """Test that info_masking_rules calls _fetch_from_cinfo_log with correct type""" 90 | from mock import MagicMock 91 | 92 | # Mock the _fetch_from_cinfo_log method 93 | fetch_mock.return_value = {"timestamp": {"node1": []}} 94 | 95 | # Create a mock handler instance 96 | handler = MagicMock(spec=CollectinfoLogHandler) 97 | handler._fetch_from_cinfo_log = fetch_mock 98 | 99 | # Call the actual method 100 | CollectinfoLogHandler.info_masking_rules(handler) 101 | 102 | # Verify it was called with the correct type 103 | fetch_mock.assert_called_once_with(type="masking") 104 | -------------------------------------------------------------------------------- /lib/live_cluster/client/msgpack.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Union 16 | from msgpack.fallback import Packer 17 | from msgpack import ExtType 18 | 19 | from lib.live_cluster.client.ctx import ASValue, ASValues, CDTContext, CTXItem, CTXItems 20 | 21 | AS_BYTES_STRING = 3 22 | AS_BYTES_BLOB = 4 23 | AS_BYTES_GEOJSON = 23 24 | ASVAL_CMP_EXT_TYPE = 0xFF 25 | ASVAL_CMP_WILDCARD = 0x00 26 | 27 | 28 | class CTXItemWireType: 29 | AS_CDT_CTX_LIST_INDEX = 0x10 30 | AS_CDT_CTX_LIST_RANK = 0x11 31 | AS_CDT_CTX_LIST_VALUE = 0x13 32 | AS_CDT_CTX_MAP_INDEX = 0x20 33 | AS_CDT_CTX_MAP_RANK = 0x21 34 | AS_CDT_CTX_MAP_KEY = 0x22 35 | AS_CDT_CTX_MAP_VALUE = 0x23 36 | 37 | 38 | class ASPacker(Packer): 39 | def __init__(self, autoreset=False): 40 | super().__init__(autoreset=autoreset) 41 | 42 | def pack(self, obj): 43 | if isinstance(obj, ASValue): 44 | self._pack_as_value(obj) 45 | return 46 | elif isinstance(obj, CDTContext): 47 | self._pack_as_cdt_ctx(obj) 48 | return 49 | 50 | super().pack(obj) 51 | 52 | def _pack_as_cdt_ctx(self, obj: CDTContext): 53 | """ 54 | For packing an ctx in order to create a secondary index. The protocol 55 | for packing a CDT with a CTX has a slightly different format. 56 | """ 57 | n = len(obj) * 2 58 | self.pack_array_header(n) 59 | 60 | for item in obj: 61 | self._pack_as_cdt_item(item) 62 | 63 | return 64 | 65 | def _pack_as_cdt_item(self, obj: CTXItem): 66 | if isinstance(obj, CTXItems.ListIndex): 67 | self.pack(CTXItemWireType.AS_CDT_CTX_LIST_INDEX) 68 | elif isinstance(obj, CTXItems.ListRank): 69 | self.pack(CTXItemWireType.AS_CDT_CTX_LIST_RANK) 70 | elif isinstance(obj, CTXItems.ListValue): 71 | self.pack(CTXItemWireType.AS_CDT_CTX_LIST_VALUE) 72 | elif isinstance(obj, CTXItems.MapIndex): 73 | self.pack(CTXItemWireType.AS_CDT_CTX_MAP_INDEX) 74 | elif isinstance(obj, CTXItems.MapRank): 75 | self.pack(CTXItemWireType.AS_CDT_CTX_MAP_RANK) 76 | elif isinstance(obj, CTXItems.MapKey): 77 | self.pack(CTXItemWireType.AS_CDT_CTX_MAP_KEY) 78 | elif isinstance(obj, CTXItems.MapValue): 79 | self.pack(CTXItemWireType.AS_CDT_CTX_MAP_VALUE) 80 | self.pack(obj.value) 81 | return 82 | 83 | def _pack_as_value(self, obj: ASValue): 84 | if isinstance(obj, ASValues.ASString): 85 | val = obj.value 86 | val = chr(AS_BYTES_STRING) + val 87 | self.pack(val) 88 | return 89 | 90 | if isinstance(obj, ASValues.ASBytes): 91 | val = obj.value 92 | val = chr(AS_BYTES_BLOB) + val.decode("utf-8") 93 | self.pack(val) 94 | return 95 | 96 | """ 97 | Not used. Here for reference in case one day they are. 98 | """ 99 | 100 | # if isinstance(obj, ASValues.ASGeoJson): 101 | # val = obj.value 102 | # val = chr(AS_BYTES_GEOJSON) + val 103 | # self.pack(val) 104 | # return 105 | 106 | # if isinstance(obj, ASValues.ASList): 107 | # val = obj.value 108 | # n = len(val) 109 | # self._pack_array_header(n) 110 | # for i in range(n): 111 | # self._pack_as_value(val[i]) 112 | # return 113 | 114 | # if isinstance(obj, ASValues.ASWildCard): 115 | # wildCardExt = ExtType(ASVAL_CMP_EXT_TYPE, ASVAL_CMP_WILDCARD) 116 | # super().pack(wildCardExt) 117 | # return 118 | 119 | self.pack(obj.value) 120 | return 121 | -------------------------------------------------------------------------------- /lib/utils/log_util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import platform 17 | 18 | DATE_SEG = 0 19 | DATE_SEPARATOR = "-" 20 | TIME_SEG = 1 21 | TIME_SEPARATOR = ":" 22 | 23 | 24 | def check_time(val, date_string, segment, index=""): 25 | try: 26 | if segment == DATE_SEG: 27 | if val.__contains__("-"): 28 | for v in range(int(val.split("-")[0]), int(val.split("-")[1]) + 1): 29 | if ( 30 | int( 31 | date_string.split(" ")[DATE_SEG].split(DATE_SEPARATOR)[ 32 | index 33 | ] 34 | ) 35 | == v 36 | ): 37 | return True 38 | 39 | elif val.__contains__(","): 40 | for v in val.split(","): 41 | if int( 42 | date_string.split(" ")[DATE_SEG].split(DATE_SEPARATOR)[index] 43 | ) == int(v): 44 | return True 45 | 46 | else: 47 | if int( 48 | date_string.split(" ")[DATE_SEG].split(DATE_SEPARATOR)[index] 49 | ) == int(val): 50 | return True 51 | elif segment == TIME_SEG: 52 | if val.__contains__("-"): 53 | for v in range(int(val.split("-")[0]), int(val.split("-")[1]) + 1): 54 | if ( 55 | int( 56 | date_string.split(" ")[TIME_SEG].split(TIME_SEPARATOR)[ 57 | index 58 | ] 59 | ) 60 | == v 61 | ): 62 | return True 63 | 64 | elif val.__contains__(","): 65 | for v in val.split(","): 66 | if int( 67 | date_string.split(" ")[TIME_SEG].split(TIME_SEPARATOR)[index] 68 | ) == int(v): 69 | return True 70 | 71 | else: 72 | if int( 73 | date_string.split(" ")[TIME_SEG].split(TIME_SEPARATOR)[index] 74 | ) == int(val): 75 | return True 76 | except Exception: 77 | pass 78 | 79 | return False 80 | 81 | 82 | def get_dirs(path=""): 83 | try: 84 | return [ 85 | name for name in os.listdir(path) if os.path.isdir(os.path.join(path, name)) 86 | ] 87 | except Exception: 88 | return [] 89 | 90 | 91 | def _is_macos_resource_fork(filename): 92 | """Check if a file is a macOS resource fork file (starts with '._')""" 93 | # skip processing Apple resource fork files https://en.wikipedia.org/wiki/AppleSingle_and_AppleDouble_formats 94 | return "darwin" in platform.system().lower() and os.path.basename( 95 | filename 96 | ).startswith("._") 97 | 98 | 99 | def get_all_files(dir_path=""): 100 | fname_list = [] 101 | if not dir_path: 102 | return fname_list 103 | try: 104 | for root, sub_dir, files in os.walk(dir_path): 105 | for fname in files: 106 | # Skip macOS resource fork files 107 | if not _is_macos_resource_fork(fname): 108 | fname_list.append(os.path.join(root, fname)) 109 | except Exception: 110 | pass 111 | 112 | return fname_list 113 | 114 | 115 | def intersect_list(a, b): 116 | return list(set(a) & set(b)) 117 | 118 | 119 | def fetch_value_from_dic(hash, keys): 120 | if not hash or not keys: 121 | return "N/E" 122 | temp_hash = hash 123 | for key in keys: 124 | if key in temp_hash: 125 | temp_hash = temp_hash[key] 126 | else: 127 | return "N/E" 128 | return temp_hash 129 | -------------------------------------------------------------------------------- /pkg/astools.conf: -------------------------------------------------------------------------------- 1 | # ----------------------------------- 2 | # Aerospike tools configuration file. 3 | # ----------------------------------- 4 | # 5 | # You can copy this to one of: 6 | # - "/etc/aerospike/astools.conf" to set global options, 7 | # - "~/.aerospike/astools.conf" to set user-specific options. 8 | # 9 | # One can use all long options that the program supports. 10 | # Run program with --help to get a list of available options. 11 | # 12 | # The commented-out settings shown in this file represent the default values. 13 | # 14 | # 15 | # Options common to all the tools 16 | # 17 | #------------------------------------------------------------------------------ 18 | # cluster specific options 19 | # 20 | # This section has connection / security / tls specific configuration optoins. 21 | # Optionally many different named connection instances can be specified. 22 | #------------------------------------------------------------------------------ 23 | # 24 | [cluster] 25 | # host = "localhost:cluster_a:3000" # host = "[:][:], ..." 26 | # user = "" 27 | # password = "" 28 | 29 | #------------------------------------------------------------------------------ 30 | # Transport Level Encryption 31 | #------------------------------------------------------------------------------ 32 | # 33 | # 34 | # tls-enable = false # true enables tls, if false all other tls 35 | # config are ignored 36 | # tls-name = "" # the tls substanza defined in your 37 | # aerospike.conf 38 | # tls-protocols = "TLSv1.2" 39 | # tls-cipher-suite = "ALL:!COMPLEMENTOFDEFAULT:!eNULL" 40 | # tls-crl-check = true 41 | # tls-crl-check-all = true 42 | 43 | # tls-keyfile = "/etc/aerospike/x509_certificates/MultiServer/key.pem" 44 | 45 | # tls-keyfile-password required if tls-keyfile is password protected 46 | # It can be one of following three format 47 | # Environment variable: "env:" 48 | # tls-keyfile-password = "env:PEMPWD" 49 | # File: "file:" 50 | # tls-keyfile-password = "file:/etc/aerospike/x509_certificates/MultiServer/keypwd.txt" 51 | # String: "" 52 | # tls-keyfile-password = "" 53 | 54 | # One of the tls-cafile or tls-capath is required. if both are specified 55 | # everything is loaded. 56 | # 57 | # tls-cafile = "/etc/aerospike/x509_certificates/Platinum/cacert.pem" 58 | # tls-capath = "/etc/aerospike/x509_certificates/Platinum" 59 | # 60 | # tls-certfile = "/etc/aerospike/x509_certificates/multi_chain.pem" 61 | 62 | [cluster_secure] 63 | # host = "localhost:cluster_a:3000" 64 | # user = "admin" 65 | # password = "admin" 66 | 67 | [cluster_tls] 68 | # host = "localhost:cluster_a:3000" 69 | # tls-enable = true 70 | # tls-name = "aerospike-tls" 71 | # tls-protocols = "-all +TLSv1.2" 72 | # tls-cipher-suite = "ALL:!COMPLEMENTOFDEFAULT:!eNULL" 73 | # tls-crl-check = true 74 | # tls-crl-check-all = true 75 | # tls-keyfile = "/etc/aerospike/x509_certificates/MultiServer/key.pem" 76 | # tls-cafile = "/etc/aerospike/x509_certificates/Platinum/cacert.pem" 77 | # tls-capath = "/etc/aerospike/x509_certificates/Platinum" 78 | # tls-certfile = "/etc/aerospike/x509_certificates/multi_chain.pem" 79 | 80 | # Following are tools specific options 81 | # 82 | # Optionally corresponding to named instance in cluster section aql can also 83 | # have instance specific config 84 | # 85 | 86 | 87 | #------------------------------------------------------------------------------ 88 | # asadm specific options 89 | #------------------------------------------------------------------------------ 90 | [asadm] 91 | # services-alumni = true 92 | # services-alternate = false 93 | # timeout = 5 94 | # enable = true 95 | 96 | [asadm_secure] 97 | # services-alternate = true 98 | # timeout = 1000 99 | 100 | 101 | #------------------------------------------------------------------------------ 102 | # CONFIG FILE INCLUDES 103 | #------------------------------------------------------------------------------ 104 | # 105 | # These options allow settings to be loaded from files other than the 106 | # default astools.conf. 107 | # 108 | # Note: 109 | # 110 | # The file in directory are read in undefined order. Avoid having 111 | # duplicate sections. 112 | # 113 | # Include file and directory are loaded after config file it is part 114 | # of. 115 | 116 | [include] 117 | # file = "./astools.conf" # include file only if it exists, 118 | # directory = "/etc/aerospike/conf.d" # directory 'conf.d' 119 | 120 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | OS = $(shell uname) 16 | SOURCE_ROOT = $(realpath .) 17 | BUILD_ROOT = $(SOURCE_ROOT)/build/ 18 | SYMLINK_ASADM = /usr/local/bin/asadm 19 | SYMLINK_ASINFO = /usr/local/bin/asinfo 20 | INSTALL_USER = aerospike 21 | INSTALL_GROUP = aerospike 22 | 23 | ifneq (,$(filter $(OS),Darwin)) 24 | INSTALL_ROOT = /usr/local/aerospike/bin/ 25 | else 26 | INSTALL_ROOT = /opt/aerospike/bin/ 27 | endif 28 | 29 | SHELL := /bin/bash 30 | 31 | define make_build 32 | mkdir -p $(BUILD_ROOT)tmp 33 | mkdir -p $(BUILD_ROOT)bin 34 | make clean 35 | cp -f *.spec $(BUILD_ROOT)tmp/ 36 | cp -f *.py $(BUILD_ROOT)tmp/ 37 | cp -rf asinfo/* $(BUILD_ROOT)tmp/ 38 | rsync -aL lib $(BUILD_ROOT)tmp/ 39 | 40 | $(if $(filter $(OS),Darwin), 41 | (git describe && sed -i "" s/[$$][$$]__version__[$$][$$]/`git describe`/g $(BUILD_ROOT)tmp/asadm.py) || true , 42 | (sed -i'' "s/[$$][$$]__version__[$$][$$]/`git describe`/g" $(BUILD_ROOT)tmp/asadm.py) || true 43 | ) 44 | 45 | 46 | $(if $(filter $(OS),Darwin), 47 | (git describe && sed -i "" s/[$$][$$]__version__[$$][$$]/`git describe`/g $(BUILD_ROOT)tmp/asinfo.py) || true , 48 | (sed -i'' "s/[$$][$$]__version__[$$][$$]/`git describe`/g" $(BUILD_ROOT)tmp/asinfo.py) || true 49 | ) 50 | 51 | endef 52 | 53 | .PHONY: default 54 | default: one-dir 55 | 56 | .PHONY: one-file 57 | one-file: init 58 | $(call make_build) 59 | pipenv run bash -c "(cd $(BUILD_ROOT)tmp && pyinstaller pyinstaller-build.spec --distpath $(BUILD_ROOT)bin -- --one-file)" 60 | @echo Check $(BUILD_ROOT)bin for asadm and asinfo executables 61 | 62 | # For macOS but can be used for any OS. 63 | .PHONY: one-dir 64 | one-dir: init 65 | $(call make_build) 66 | pipenv run bash -c "(cd $(BUILD_ROOT)tmp && pyinstaller pyinstaller-build.spec --distpath $(BUILD_ROOT)bin)" 67 | mv $(BUILD_ROOT)bin/asinfo/asinfo $(BUILD_ROOT)bin/asadm/asinfo 68 | rm -r $(BUILD_ROOT)bin/asinfo 69 | @echo Check $(BUILD_ROOT)bin for bundle 70 | 71 | .PHONY: init 72 | init: 73 | pipenv install --dev 74 | # pipenv check 75 | pipenv graph 76 | 77 | UNIT_TEST_CMD=pytest --disable-warnings test/unit 78 | E2E_TEST_CMD=pytest --disable-warnings test/e2e/live_cluster 79 | COVERAGE_CONF=$(SOURCE_ROOT)/tox.ini 80 | 81 | .PHONY: unit 82 | unit: 83 | $(UNIT_TEST_CMD) 84 | 85 | .PHONY: integration 86 | integration: 87 | FEATKEY=$(FEATKEY) $(E2E_TEST_CMD) 88 | 89 | .PHONY: unit-cov 90 | unit-cov: 91 | coverage run --module $(UNIT_TEST_CMD) 92 | 93 | .PHONY: integration-cov 94 | integration-cov: 95 | COVERAGE_PROCESS_START=$(COVERAGE_CONF) FEATKEY=$(FEATKEY) coverage run --module $(E2E_TEST_CMD) 96 | 97 | .PHONY: coverage 98 | coverage: 99 | coverage erase 100 | make unit-cov 101 | make integration-cov 102 | coverage combine 103 | 104 | .PHONY: install 105 | install: uninstall 106 | install -d -m 755 $(INSTALL_ROOT) 107 | ifneq ($(wildcard $(BUILD_ROOT)bin/asadm/*),) 108 | @echo "Asadm and Asinfo were built in one-dir mode" 109 | cp -r $(BUILD_ROOT)bin/asadm $(INSTALL_ROOT)asadm 110 | ln -sf $(INSTALL_ROOT)asadm/asadm $(SYMLINK_ASADM) 111 | ln -sf $(INSTALL_ROOT)asadm/asinfo $(SYMLINK_ASINFO) 112 | else 113 | @echo "Asadm and Asinfo were built in one-file mode" 114 | install -m 755 $(BUILD_ROOT)bin/asadm $(INSTALL_ROOT)asadm 115 | install -m 755 $(BUILD_ROOT)bin/asinfo $(INSTALL_ROOT)asinfo 116 | ln -sf $(INSTALL_ROOT)asadm $(SYMLINK_ASADM) 117 | ln -sf $(INSTALL_ROOT)asinfo $(SYMLINK_ASINFO) 118 | endif 119 | 120 | .PHONY: uninstall 121 | uninstall: 122 | rm -r $(INSTALL_ROOT)asadm || true 123 | rm -r $(INSTALL_ROOT)asinfo || true 124 | rm $(SYMLINK_ASADM) || true 125 | rm $(SYMLINK_ASINFO) || true 126 | 127 | 128 | .PHONY: clean 129 | clean: 130 | rm -rf $(BUILD_ROOT)tmp/* 131 | rm -rf $(BUILD_ROOT)bin/* 132 | rm -f `find . -type f -name '*.pyc' | xargs` 133 | pipenv clean 134 | 135 | 136 | .PHONY: format-check 137 | format-check: 138 | pipenv run black . --check --diff 139 | 140 | .PHONY: format 141 | format: 142 | pipenv run black . -------------------------------------------------------------------------------- /lib/view/sheet/render/json_rsheet.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | 17 | from .base_rsheet import BaseRField, BaseRSheet, BaseRSubgroup, ErrorEntry, NoEntry 18 | 19 | 20 | class JSONRSheet(BaseRSheet): 21 | def do_create_tuple_field(self, field, groups): 22 | return RSubgroupJSON(self, field, groups) 23 | 24 | def do_create_field(self, field, groups, parent_key=None): 25 | return RFieldJSON(self, field, groups, parent_key=parent_key) 26 | 27 | def do_render(self): 28 | rfields = self.visible_rfields 29 | 30 | groups = [] 31 | result = dict(title=self.title, groups=groups) 32 | 33 | if self.description: 34 | result["description"] = self.description 35 | 36 | if len(rfields) == 0: 37 | return json.dumps(result, indent=2) 38 | 39 | n_groups = 0 if not rfields else rfields[0].n_groups 40 | 41 | for group_ix in range(n_groups): 42 | records = [] 43 | aggregates = {} 44 | group = dict(records=records) 45 | 46 | groups.append(group) 47 | 48 | for entry_ix in range(rfields[0].n_entries_in_group(group_ix)): 49 | record = {} 50 | 51 | records.append(record) 52 | 53 | for rfield in rfields: 54 | field_key = rfield.decleration.key 55 | 56 | if rfield.is_tuple_field: 57 | tuple_field = {} 58 | record[field_key] = tuple_field 59 | 60 | for rsubfield in rfield.visible: 61 | self.do_render_field( 62 | rsubfield, group_ix, entry_ix, tuple_field 63 | ) 64 | else: 65 | self.do_render_field(rfield, group_ix, entry_ix, record) 66 | 67 | for rfield in rfields: 68 | field_key = rfield.decleration.key 69 | 70 | if rfield.is_tuple_field: 71 | tuple_agg = {} 72 | 73 | for rsubfield in rfield.visible: 74 | self.do_render_aggregate(rsubfield, group_ix, tuple_agg) 75 | 76 | if tuple_agg: 77 | aggregates[field_key] = tuple_agg 78 | else: 79 | self.do_render_aggregate(rfield, group_ix, aggregates) 80 | 81 | if aggregates: 82 | group["aggregates"] = aggregates 83 | 84 | return json.dumps(result, indent=2) 85 | 86 | def do_render_field(self, rfield, group_ix, entry_ix, record): 87 | value = rfield.groups[group_ix][entry_ix] 88 | converted_value = rfield.groups_converted[group_ix][entry_ix].strip() 89 | 90 | if value is ErrorEntry: 91 | value = "error" 92 | elif value is NoEntry: 93 | value = "null" 94 | 95 | key = rfield.decleration.key # use key, instead of title, for uniqueness 96 | record[key] = dict(raw=value, converted=converted_value) 97 | format_name, _ = rfield.entry_format(group_ix, entry_ix) 98 | 99 | if format_name is not None: 100 | record[key]["format"] = format_name 101 | 102 | def do_render_aggregate(self, rfield, group_ix, aggregates): 103 | aggregate = rfield.aggregates[group_ix] 104 | converted_aggregate = rfield.aggregates_converted[group_ix].strip() 105 | 106 | if aggregate is ErrorEntry: 107 | aggregate = "error" 108 | elif aggregate is NoEntry: 109 | aggregate = "null" 110 | 111 | key = rfield.decleration.key # use key, instead of title, for uniqueness 112 | 113 | if aggregate is not None: 114 | aggregates[key] = dict(raw=aggregate, converted=converted_aggregate) 115 | 116 | 117 | class RSubgroupJSON(BaseRSubgroup): 118 | pass 119 | 120 | 121 | class RFieldJSON(BaseRField): 122 | pass 123 | -------------------------------------------------------------------------------- /lib/live_cluster/client/ssl_util.py: -------------------------------------------------------------------------------- 1 | # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 2 | # -------------------------------------------- 3 | # 4 | # 1. This LICENSE AGREEMENT is between the Python Software Foundation 5 | # ("PSF"), and the Individual or Organization ("Licensee") accessing and 6 | # otherwise using this software ("Python") in source or binary form and 7 | # its associated documentation. 8 | # 9 | # 2. Subject to the terms and conditions of this License Agreement, PSF hereby 10 | # grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 11 | # analyze, test, perform and/or display publicly, prepare derivative works, 12 | # distribute, and otherwise use Python alone or in any derivative version, 13 | # provided, however, that PSF's License Agreement and PSF's notice of copyright, 14 | # i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 15 | # 2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are 16 | # retained in Python alone or in any derivative version prepared by Licensee. 17 | # 18 | # 3. In the event Licensee prepares a derivative work that is based on 19 | # or incorporates Python or any part thereof, and wants to make 20 | # the derivative work available to others as provided herein, then 21 | # Licensee hereby agrees to include in any such work a brief summary of 22 | # the changes made to Python. 23 | # 24 | # 4. PSF is making Python available to Licensee on an "AS IS" 25 | # basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 26 | # IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 27 | # DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 28 | # FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 29 | # INFRINGE ANY THIRD PARTY RIGHTS. 30 | # 31 | # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 32 | # FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 33 | # A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 34 | # OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 35 | # 36 | # 6. This License Agreement will automatically terminate upon a material 37 | # breach of its terms and conditions. 38 | # 39 | # 7. Nothing in this License Agreement shall be deemed to create any 40 | # relationship of agency, partnership, or joint venture between PSF and 41 | # Licensee. This License Agreement does not grant permission to use PSF 42 | # trademarks or trade name in a trademark sense to endorse or promote 43 | # products or services of Licensee, or any third party. 44 | # 45 | # 8. By copying, installing or otherwise using Python, Licensee 46 | # agrees to be bound by the terms and conditions of this License 47 | # Agreement. 48 | import re 49 | 50 | 51 | def dnsname_match(dn, hostname, max_wildcards=1): 52 | """Matching according to RFC 6125, section 6.4.3 53 | 54 | http://tools.ietf.org/html/rfc6125#section-6.4.3 55 | """ 56 | pats = [] 57 | if not dn: 58 | return False 59 | 60 | p = dn.split(r".") 61 | leftmost = p[0] 62 | remainder = p[1:] 63 | wildcards = leftmost.count("*") 64 | if wildcards > max_wildcards: 65 | # Issue #17980: avoid denials of service by refusing more 66 | # than one wildcard per fragment. A survery of established 67 | # policy among SSL implementations showed it to be a 68 | # reasonable choice. 69 | raise Exception("too many wildcards in certificate Subject: " + repr(dn)) 70 | 71 | # speed up common case w/o wildcards 72 | if not wildcards: 73 | return dn.lower() == hostname.lower() 74 | 75 | # RFC 6125, section 6.4.3, subitem 1. 76 | # The client SHOULD NOT attempt to match a presented identifier in which 77 | # the wildcard character comprises a label other than the left-most label. 78 | if leftmost == "*": 79 | # When '*' is a fragment by itself, it matches a non-empty dotless 80 | # fragment. 81 | pats.append("[^.]+") 82 | elif leftmost.startswith("xn--") or hostname.startswith("xn--"): 83 | # RFC 6125, section 6.4.3, subitem 3. 84 | # The client SHOULD NOT attempt to match a presented identifier 85 | # where the wildcard character is embedded within an A-label or 86 | # U-label of an internationalized domain name. 87 | pats.append(re.escape(leftmost)) 88 | else: 89 | # Otherwise, '*' matches any dotless string, e.g. www* 90 | pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) 91 | 92 | # add the remaining fragments, ignore any wildcards 93 | for frag in remainder: 94 | pats.append(re.escape(frag)) 95 | 96 | pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) 97 | return pat.match(hostname) 98 | -------------------------------------------------------------------------------- /test/test_asinfo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2013-2025 Aerospike, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | run_test(){ 18 | unknown_option_error="Do not understand" 19 | asinfo_cmd_str="'$1' " 20 | # echo ${asinfo_cmd_str} 21 | cmd_out=`./asadm.py --asinfo-mode --no-config-file -e "${asinfo_cmd_str}" -Uadmin -Padmin` 22 | cmd_status="$?" 23 | # echo ${cmd_out} 24 | if [ "$cmd_status" -ne 0 ]; then 25 | # echo 26 | return 1 27 | fi 28 | if [[ $cmd_out == *"${unknown_option_error}"* ]]; then 29 | # echo 30 | return 1 31 | fi 32 | if [[ $cmd_out != *"$2"* ]];then 33 | # echo 34 | return 1 35 | fi 36 | echo -n "." 37 | } 38 | 39 | asinfo_cmd="bins" 40 | output_substring="bin" 41 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 42 | echo "Error while running asinfo command: ${asinfo_cmd}" 43 | exit 1 44 | fi 45 | 46 | asinfo_cmd="bins/test" 47 | output_substring="bin" 48 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 49 | echo "Error while running asinfo command: ${asinfo_cmd}" 50 | exit 1 51 | fi 52 | 53 | asinfo_cmd="get-config:context=namespace;id=test" 54 | output_substring="default-ttl" 55 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 56 | echo "Error while running asinfo command: ${asinfo_cmd}" 57 | exit 1 58 | fi 59 | 60 | # Deprecated with server 5.0, replaced with get-config below 61 | # asinfo_cmd="get-dc-config" 62 | # output_substring1="dc-name" 63 | # output_substring2="DC_Name" 64 | # if ( ! run_test ${asinfo_cmd} ${output_substring1} ) && ( ! run_test ${asinfo_cmd} ${output_substring2} ) ; then 65 | # echo "Error while running asinfo command: ${asinfo_cmd}" 66 | # exit 1 67 | # fi 68 | 69 | asinfo_cmd="get-config:context=xdr" 70 | output_substring1="dcs" 71 | if ( ! run_test ${asinfo_cmd} ${output_substring1} ) ; then 72 | echo "Error while running asinfo command: ${asinfo_cmd}" 73 | exit 1 74 | fi 75 | 76 | asinfo_cmd="hist-dump:ns=test;hist=ttl" 77 | output_substring="test:ttl" 78 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 79 | asinfo_cmd="histogram:namespace=test;type=ttl" 80 | output_substring="units=seconds:" 81 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 82 | echo "Error while running asinfo command: ${asinfo_cmd}" 83 | exit 1 84 | fi 85 | fi 86 | 87 | asinfo_cmd="latencies:" 88 | output_substring="test" 89 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 90 | echo "Error while running asinfo command: ${asinfo_cmd}" 91 | exit 1 92 | fi 93 | 94 | asinfo_cmd="log/0" 95 | output_substring="fabric:INFO" 96 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 97 | echo "Error while running asinfo command: ${asinfo_cmd}" 98 | exit 1 99 | fi 100 | 101 | asinfo_cmd="namespace/test" 102 | output_substring="prole" 103 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 104 | echo "Error while running asinfo command: ${asinfo_cmd}" 105 | exit 1 106 | fi 107 | 108 | asinfo_cmd="get-config:context=network" 109 | output_substring="address" 110 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 111 | echo "Error while running asinfo command: ${asinfo_cmd}" 112 | exit 1 113 | fi 114 | 115 | asinfo_cmd="STATUS" 116 | output_substring="OK" 117 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 118 | echo "Error while running asinfo command: ${asinfo_cmd}" 119 | exit 1 120 | fi 121 | 122 | # Test for invisible escape characters which can get added due to any python library like readline 123 | 124 | asinfo_cmd_str="\"STATUS\" " 125 | 126 | cmd_out=`./asadm.py -Uadmin -Padmin --asinfo-mode --no-config-file -e "${asinfo_cmd_str}" | tr -dc '[:alnum:]\n\r'` 127 | cmd_status="$?" 128 | 129 | if [ "$cmd_status" -ne 0 ]; then 130 | echo "Error: translate command over asinfo output failed" 131 | exit 1 132 | fi 133 | if [[ $cmd_out != "OK" ]];then 134 | echo "Error: extra characters in asinfo output. Expected OK but output is ${cmd_out}" 135 | exit 1 136 | fi 137 | 138 | cmd_out=`./asadm.py -Uadmin -Padmin --asinfo-mode --no-config-file -e "${asinfo_cmd_str}" | hexdump` 139 | cmd_status="$?" 140 | expected_output=`echo "OK" | hexdump` 141 | 142 | if [ "$cmd_status" -ne 0 ]; then 143 | echo "Error: hexdump command over asinfo output failed" 144 | exit 1 145 | fi 146 | if [[ $cmd_out != $expected_output ]];then 147 | echo "Error: hexdump command over asinfo output failed" 148 | exit 1 149 | fi 150 | 151 | echo 152 | echo "OK" 153 | exit 0 154 | -------------------------------------------------------------------------------- /asinfo/run_asinfo_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | run_test(){ 4 | unknown_option_error="Do not understand" 5 | cmd_out=`./asinfo.py -v "$1"` 6 | cmd_status="$?" 7 | # echo ${cmd_out} 8 | if [ "$cmd_status" -ne 0 ]; then 9 | return 1 10 | fi 11 | if [[ $cmd_out == *"${unknown_option_error}"* ]]; then 12 | return 1 13 | fi 14 | if [[ $cmd_out != *"$2"* ]];then 15 | return 1 16 | fi 17 | echo -n "." 18 | } 19 | 20 | asinfo_cmd="bins" 21 | output_substring="bin" 22 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 23 | echo "Error while running asinfo command: ${asinfo_cmd}" 24 | exit 1 25 | fi 26 | 27 | asinfo_cmd="bins/test" 28 | output_substring="bin" 29 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 30 | echo "Error while running asinfo command: ${asinfo_cmd}" 31 | exit 1 32 | fi 33 | 34 | asinfo_cmd="get-config:context=namespace;id=test" 35 | output_substring="default-ttl" 36 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 37 | echo "Error while running asinfo command: ${asinfo_cmd}" 38 | exit 1 39 | fi 40 | 41 | asinfo_cmd="get-dc-config" 42 | output_substring1="dc-name" 43 | output_substring2="DC_Name" 44 | if ( ! run_test ${asinfo_cmd} ${output_substring1} ) && ( ! run_test ${asinfo_cmd} ${output_substring2} ) ; then 45 | echo "Error while running asinfo command: ${asinfo_cmd}" 46 | exit 1 47 | fi 48 | 49 | asinfo_cmd="hist-dump:ns=test;hist=ttl" 50 | output_substring="test:ttl" 51 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 52 | echo "Error while running asinfo command: ${asinfo_cmd}" 53 | exit 1 54 | fi 55 | 56 | asinfo_cmd="latency:" 57 | output_substring="test" 58 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 59 | echo "Error while running asinfo command: ${asinfo_cmd}" 60 | exit 1 61 | fi 62 | 63 | asinfo_cmd="log/0" 64 | output_substring="fabric:INFO" 65 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 66 | echo "Error while running asinfo command: ${asinfo_cmd}" 67 | exit 1 68 | fi 69 | 70 | asinfo_cmd="namespace/test" 71 | output_substring="prole" 72 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 73 | echo "Error while running asinfo command: ${asinfo_cmd}" 74 | exit 1 75 | fi 76 | 77 | asinfo_cmd="get-config:context=network" 78 | output_substring="address" 79 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 80 | echo "Error while running asinfo command: ${asinfo_cmd}" 81 | exit 1 82 | fi 83 | 84 | asinfo_cmd="STATUS" 85 | output_substring="OK" 86 | if ! run_test ${asinfo_cmd} ${output_substring} ; then 87 | echo "Error while running asinfo command: ${asinfo_cmd}" 88 | exit 1 89 | fi 90 | 91 | # Test cases for invisible escape characters which can get added due to any python library like readline 92 | 93 | cmd_out=`./asinfo.py -v "STATUS" | tr -dc '[:alnum:]\n\r'` 94 | cmd_status="$?" 95 | 96 | if [ "$cmd_status" -ne 0 ]; then 97 | echo "Error: translate command over asinfo output failed" 98 | exit 1 99 | fi 100 | if [[ $cmd_out != "OK" ]];then 101 | echo "Error: extra characters in asinfo output. Expected OK but output is ${cmd_out}" 102 | exit 1 103 | fi 104 | 105 | cmd_out=`./asinfo.py -v "STATUS" | hexdump` 106 | cmd_status="$?" 107 | expected_output=`echo "OK" | hexdump` 108 | 109 | if [ "$cmd_status" -ne 0 ]; then 110 | echo "Error: hexdump command over asinfo output failed" 111 | exit 1 112 | fi 113 | if [[ $cmd_out != $expected_output ]];then 114 | echo "Error: hexdump command over asinfo output failed" 115 | exit 1 116 | fi 117 | 118 | # Test cases for asadm error handling 119 | 120 | ## Wrong Host 121 | expected_error_msg="request to WrongHost : 3000 returned error" 122 | cmd_out=`./asinfo.py -h "WrongHost"` 123 | cmd_status="$?" 124 | 125 | if [ "$cmd_status" -ne 1 ]; then 126 | echo "Error: asinfo with wrong Host failed" 127 | exit 1 128 | fi 129 | #if [[ $cmd_out != $expected_error_msg ]];then 130 | # echo "Error: asinfo with wrong Host failed. Expected '${expected_error_msg}' but output is '${cmd_out}'" 131 | # exit 1 132 | #fi 133 | 134 | # Wrong Port 135 | expected_error_msg="request to 127.0.0.1 : 98989898 returned error" 136 | cmd_out=`./asinfo.py -h "localhost" -p 98989898` 137 | cmd_status="$?" 138 | 139 | if [ "$cmd_status" -ne 1 ]; then 140 | echo "Error: asinfo with wrong Port failed" 141 | exit 1 142 | fi 143 | #if [[ $cmd_out != $expected_error_msg ]];then 144 | # echo "Error: asinfo with wrong Port failed. Expected '${expected_error_msg}' but output is '${cmd_out}'" 145 | # exit 1 146 | #fi 147 | 148 | # Wrong command value 149 | expected_error_msg="Error: Invalid command 'WrongCommand'" 150 | cmd_out=`./asinfo.py -v "WrongCommand"` 151 | cmd_status="$?" 152 | 153 | if [ "$cmd_status" -ne 1 ]; then 154 | echo "Error: asinfo with wrong Command Value failed" 155 | exit 1 156 | fi 157 | if [[ $cmd_out != $expected_error_msg ]];then 158 | echo "Error: asinfo with wrong Command Value failed. Expected '${expected_error_msg}' but output is '${cmd_out}'" 159 | exit 1 160 | fi 161 | 162 | echo 163 | echo "All asinfo Test passed Successfully" 164 | exit 0 -------------------------------------------------------------------------------- /.github/workflows/mac-artifact.yml: -------------------------------------------------------------------------------- 1 | name: Mac Artifact 2 | 3 | on: 4 | repository_dispatch: 5 | types: mac-build 6 | push: 7 | branches: [actionsHub, master, "bugfix-*"] 8 | pull_request: 9 | branches: [master] 10 | workflow_call: 11 | inputs: 12 | submodule: 13 | description: The directory of the submodule, if this workflow is being called on a submodule 14 | required: false 15 | type: string 16 | 17 | jobs: 18 | build: 19 | strategy: 20 | matrix: 21 | os: [macos-14, macos-15, macos-26] 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Harden the runner (Audit all outbound calls) 25 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 26 | with: 27 | egress-policy: audit 28 | 29 | - name: Get checkout directory 30 | uses: step-security/action-cond@45aa5a709bd075f11c74285d8b0ca4d527e5fbcf # v1.2.4 31 | id: checkout-dir 32 | with: 33 | cond: ${{ inputs.submodule != '' }} 34 | if_true: aerospike-tools # In this case we are expecting to checkout the tools package. 35 | if_false: admin 36 | - name: Get asadm working directory 37 | uses: step-security/action-cond@45aa5a709bd075f11c74285d8b0ca4d527e5fbcf # v1.2.4 38 | id: working-dir 39 | with: 40 | cond: ${{ inputs.submodule != '' }} 41 | if_true: aerospike-tools/${{ inputs.submodule }} # In this case we are expecting to checkout the tools package. 42 | if_false: admin 43 | - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 44 | with: 45 | path: ${{ steps.checkout-dir.outputs.value }} 46 | fetch-depth: 0 47 | - name: Checkout ${{ steps.working-dir.outputs.value }} 48 | working-directory: ${{ steps.checkout-dir.outputs.value }} 49 | run: | 50 | git config --global url."https://github.com/".insteadOf "git@github.com:" 51 | git submodule update --init --recursive -- ${{ inputs.submodule || '.' }} 52 | - name: Print and get version 53 | working-directory: ${{ steps.working-dir.outputs.value }} 54 | id: tag 55 | run: | 56 | git describe --tags --always 57 | echo "tag=$(git describe --tags --always)" >> $GITHUB_OUTPUT 58 | - uses: kenchan0130/actions-system-info@b98fe44bc35efcc60a9828cf4a04783a8b370bde # v1.3.0 59 | id: system-info 60 | - name: Get Python version from Pipfile 61 | working-directory: ${{ steps.working-dir.outputs.value }} 62 | run: | 63 | git rev-parse HEAD 64 | echo "PYTHON_VERSION=$(grep "python_version" Pipfile | cut -d ' ' -f 3 - | tr -d '"')" >> $GITHUB_ENV 65 | echo ${{ steps.system-info.outputs.name }} 66 | echo ${{ steps.system-info.outputs.kernel-release }} 67 | echo ${{ steps.system-info.outputs.platform }} 68 | - name: Setup Python 69 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 70 | with: 71 | python-version: ${{ env.PYTHON_VERSION }} 72 | - name: Cache asadm and asinfo 73 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 74 | id: cache-asadm-asinfo 75 | env: 76 | cache-name: cache-asadm-asinfo 77 | cache-index: "4" 78 | with: 79 | path: | 80 | ${{ steps.working-dir.outputs.value }}/build/bin 81 | key: ${{ env.cache-name }}-${{ env.cache-index }}-${{ matrix.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.release }}-${{ env.PYTHON_VERSION }}-${{ steps.tag.outputs.tag }} 82 | - name: Pipenv setup 83 | if: steps.cache-asadm-asinfo.outputs.cache-hit != 'true' 84 | working-directory: ${{ steps.working-dir.outputs.value }} 85 | run: | 86 | brew install pipenv 87 | pipenv install --dev 88 | - name: Build asadm 89 | if: steps.cache-asadm-asinfo.outputs.cache-hit != 'true' 90 | working-directory: ${{ steps.working-dir.outputs.value }} 91 | run: | 92 | make one-dir 93 | ls -al build/bin/asadm || true 94 | # - name: Setup tmate session 95 | # uses: mxschmitt/action-tmate@v3 96 | - name: Sanity Test tools 97 | working-directory: ${{ steps.working-dir.outputs.value }} 98 | run: | 99 | sudo make install 100 | asadm -e "info" || true 101 | asadm -e "info" 2>&1 | grep "Not able to connect" 102 | asinfo || true 103 | asinfo 2>&1 | grep "Not able to connect" 104 | - name: Create .tar 105 | working-directory: ${{ steps.working-dir.outputs.value }} 106 | run: | 107 | tar -C build/bin/ -cvf asadm.tar asadm 108 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 109 | with: 110 | name: ${{ steps.system-info.outputs.platform }}-${{ runner.arch }}-${{ matrix.os }}-${{ steps.system-info.outputs.release }}-asadm 111 | path: ${{ steps.working-dir.outputs.value }}/asadm.tar 112 | if-no-files-found: error 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aerospike Admin 2 | 3 | ## Description 4 | Aerospike Admin provides an interface for Aerospike users to view the stat 5 | of their Aerospike Cluster by fetching information from a running cluster (Cluster mode) 6 | a collectinfo file (Collectinfo-Analyzer), or logs (Log-analyser mode). 7 | To get started run `asadm` and issue the `help` command. The 8 | full documentation can be found [here](https://docs.aerospike.com/tools/asadm). 9 | 10 | ## Asinfo 11 | The Aerospike Admin repo now contains asinfo. Asinfo has been a long time member of the 12 | Aerospike Tools package and is now built together with asadm. Asinfo provides a raw 13 | interface to Aerospike info protocol and is useful for debugging and development. The 14 | full documentation can be found [here](https://docs.aerospike.com/tools/asinfo). 15 | 16 | ## Build and Install Aerospike Admin 17 | ### Runtime Dependencies 18 | There are no runtime dependencies. This is because the python interpreter is now 19 | bundled with asadm version 2.6 and later. 20 | 21 | ### Build Dependencies 22 | - python 3.10 23 | - pipenv 24 | 25 | ### Build and Install Asadm 26 | 1. Install python 3.10 27 | 2. Install [pipenv](https://pypi.org/project/pipenv/) 28 | 3. Initialize submodules `git submodule update --init` 29 | 4. Build Asadm 30 | - There are two ways asadm can be bundled: one-file and one-dir. Both, are related to 31 | pyinstaller's two methods of bundling. The one-file build is great if you want a single 32 | executable artifact. The downside of one-file is that it must decompress into a /tmp 33 | directory in order to execute. This causes a number of problems. On macOS, the startup 34 | time is drastically increased because of the codesigning mechanism. The /tmp directory 35 | must be mounted with the the exec option. If the above scenarios describe your environment 36 | then use the one-dir build. 37 | * one-dir (default) 38 | ``` 39 | make one-dir 40 | ``` 41 | * one-file 42 | ``` 43 | make one-file 44 | ``` 45 | 5. Install asadm 46 | ``` 47 | sudo make install 48 | ``` 49 | 50 | ## Running Aerospike Admin in Live Cluster Mode. 51 | ``` 52 | asadm -h 53 | Admin> help 54 | ``` 55 | 56 | ## Running Aerospike Admin in Log-analyser Mode. 57 | ``` 58 | asadm -l [-f ] 59 | Admin> help 60 | ``` 61 | 62 | ## Running Aerospike Admin in Collectinfo Mode. 63 | ``` 64 | asadm -c [-f ] 65 | Admin> help 66 | ``` 67 | 68 | ## Contributing 69 | 70 | To contribute to Aerospike Admin, please follow the code style and quality guidelines outlined below. 71 | 72 | ### Code Style and Quality 73 | 74 | To maintain consistent code style and quality, Aerospike Admin uses [Black](https://black.readthedocs.io/en/stable/) and [Flake8](https://flake8.pycqa.org/en/latest/). 75 | 76 | **Checking Code Style and Linting Issues:** 77 | 78 | To check if your code adheres to the defined style and to identify potential linting issues without making any changes to the files: 79 | 80 | ```bash 81 | make format-check 82 | ``` 83 | 84 | This command will use Black to check formatting (and show diffs for any non-compliant files) and Flake8 to report on style guide violations and potential bugs. 85 | 86 | **Automatically Fixing Code Style:** 87 | 88 | To automatically reformat your code according to project standards: 89 | 90 | ```bash 91 | make format 92 | ``` 93 | 94 | This command will format your code using Black. 95 | 96 | **Automated Checks in Pull Requests:** 97 | 98 | A GitHub Actions workflow is configured to automatically run `make format-check` on every push to any branch and on pull requests targeting the `master` branch. This ensures that all submitted code adheres to the project's style guidelines. 99 | 100 | ## Tests 101 | Asadm has unit, integration, and e2e tests. Tests also support code coverage reporting. 102 | 103 | ### Dependencies 104 | - The e2e tests depend on docker. 105 | - The e2e tests depend on a aerospike features.conf file to run Aerospike Enterprise Edition. 106 | - Run `pipenv install --dev`. 107 | 108 | ### Running Unit Tests 109 | ``` 110 | make unit 111 | ``` 112 | 113 | ### Running e2e/integration Tests 114 | ``` 115 | FEATKEY=(base64 -i path/to/features.conf) make integration 116 | ``` 117 | to test the bundled app run: 118 | ``` 119 | ASADM_TEST_BUNDLE=true FEATKEY=(base64 -i path/to/features.conf) make integration 120 | ``` 121 | 122 | **Note for Mac users**: If you encounter Docker networking issues on macOS, enable localhost networking: 123 | ``` 124 | E2E_TEST_USE_LOCALHOST=true FEATKEY=(base64 -i path/to/features.conf) make integration 125 | ``` 126 | 127 | **Debugging with Docker logs**: To capture and save Docker container logs during e2e tests for debugging: 128 | ``` 129 | DUMP_DOCKER_LOGS=true FEATKEY=(base64 -i path/to/features.conf) make integration 130 | ``` 131 | This will save container logs to the `docker_logs/` directory when containers are stopped. 132 | 133 | ### Running all tests with coverage 134 | ``` 135 | FEATKEY=(base64 -i path/to/features.conf) make coverage 136 | ``` 137 | 138 | ## Profiling 139 | ### Dependencies 140 | - yappi 141 | 142 | ### Run Profiler 143 | asadm --profile 144 | Do not exit with 'ctrl+c' exit with the *exit* command 145 | -------------------------------------------------------------------------------- /.github/workflows/build-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: build-artifacts.yml 2 | permissions: 3 | contents: read 4 | id-token: write 5 | attestations: write 6 | on: 7 | workflow_dispatch: 8 | pull_request: 9 | # yamllint disable-line rule:quoted-strings 10 | branches: 11 | - "*" 12 | push: 13 | branches: 14 | # yamllint disable-line rule:quoted-strings 15 | - "release/*" 16 | # yamllint disable-line rule:quoted-strings 17 | - "main" 18 | # yamllint disable-line rule:quoted-strings 19 | - "master" 20 | # yamllint disable-line rule:quoted-strings 21 | - "develop" 22 | 23 | 24 | jobs: 25 | build-artifacts: 26 | strategy: 27 | matrix: 28 | distro: 29 | - el8 30 | - el9 31 | - el10 32 | - amzn2023 33 | - debian12 34 | - debian13 35 | - ubuntu20.04 36 | - ubuntu22.04 37 | - ubuntu24.04 38 | host: 39 | - ubuntu-24.04 40 | - ubuntu-24.04-arm 41 | uses: aerospike/shared-workflows/.github/workflows/reusable_execute-build.yaml@v2.0.2 42 | with: 43 | runs-on: ${{ matrix.host }} 44 | jf-project: database 45 | jf-build-id: ${{ github.run_number }} 46 | # this is the default behaviour so we can leave it out but if it is wanted to be explicit 47 | # we need to use the ref not the ref_name in github actions. The ref_name is the short name and so not a valid ref. 48 | # gh-source-ref: ${{ github.ref }} 49 | build-script: local/.github/packaging/project/gha-main.sh "${{ matrix.distro }}" 50 | 51 | gh-artifact-directory: dist 52 | gh-artifact-name: unsigned-artifacts-${{ matrix.distro }}-${{ matrix.host }} 53 | gh-retention-days: 1 54 | dry-run: false 55 | oidc-provider-name: database-gh-aerospike 56 | oidc-audience: database-gh-aerospike 57 | jf-build-name: ${{ github.event.repository.name }} 58 | sign-artifacts: 59 | strategy: 60 | matrix: 61 | distro: 62 | - el8 63 | - el9 64 | - el10 65 | - amzn2023 66 | - debian12 67 | - debian13 68 | - ubuntu20.04 69 | - ubuntu22.04 70 | - ubuntu24.04 71 | host: 72 | - ubuntu-24.04 73 | - ubuntu-24.04-arm 74 | needs: build-artifacts 75 | uses: aerospike/shared-workflows/.github/workflows/reusable_sign-artifacts.yaml@v2.0.2 76 | with: 77 | gh-artifact-name: signed-artifacts-${{ matrix.distro }}-${{ matrix.host }} 78 | gh-unsigned-artifacts: unsigned-artifacts-${{ matrix.distro }}-${{ matrix.host }} 79 | secrets: 80 | gpg-private-key: ${{ secrets.GPG_SECRET_KEY }} 81 | gpg-public-key: ${{ secrets.GPG_PUBLIC_KEY }} 82 | gpg-key-pass: ${{ secrets.GPG_PASS }} 83 | upload-artifacts: 84 | strategy: 85 | matrix: 86 | distro: 87 | - el8 88 | - el9 89 | - el10 90 | - amzn2023 91 | - debian12 92 | - debian13 93 | - ubuntu20.04 94 | - ubuntu22.04 95 | - ubuntu24.04 96 | host: 97 | - ubuntu-24.04 98 | - ubuntu-24.04-arm 99 | needs: sign-artifacts 100 | uses: aerospike/shared-workflows/.github/workflows/reusable_deploy-artifacts.yaml@v2.0.2 101 | with: 102 | jf-project: database 103 | jf-build-name: ${{ github.event.repository.name }} 104 | version: ${{ github.ref_name }} 105 | oidc-provider-name: database-gh-aerospike 106 | oidc-audience: database-gh-aerospike 107 | gh-artifact-name: signed-artifacts-${{ matrix.distro }}-${{ matrix.host }} 108 | gh-retention-days: 1 109 | dry-run: false 110 | jf-build-id: ${{ github.run_number }} 111 | jf-metadata-build-id: ${{ github.run_number }}-metadata 112 | test-install-from-jfrog-and-execute: 113 | strategy: 114 | matrix: 115 | distro: 116 | - el8 117 | - el9 118 | - el10 119 | - amzn2023 120 | - debian12 121 | - debian13 122 | - ubuntu20.04 123 | - ubuntu22.04 124 | - ubuntu24.04 125 | host: 126 | - ubuntu-24.04 127 | - ubuntu-24.04-arm 128 | needs: upload-artifacts 129 | env: 130 | JFROG_CLI_BUILD_NAME: ${{ inputs.jf-build-name || github.workflow }} 131 | JFROG_CLI_LOG_LEVEL: INFO 132 | runs-on: ${{ matrix.host }} 133 | permissions: 134 | contents: read 135 | packages: write 136 | attestations: write 137 | id-token: write 138 | steps: 139 | - name: Harden the runner (Audit all outbound calls) 140 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 141 | with: 142 | egress-policy: audit 143 | 144 | - name: Install JFrog CLI 145 | id: jf 146 | uses: step-security/setup-jfrog-cli@a6b41f8338bea0983ddff6bd4ede7d2dcd81e1fa # v4.8.1 147 | env: 148 | JF_URL: https://artifact.aerospike.io 149 | JF_PROJECT: database 150 | with: 151 | oidc-provider-name: database-gh-aerospike 152 | oidc-audience: database-gh-aerospike 153 | - name: Checkout 154 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 155 | with: 156 | ref: ${{ github.ref }} 157 | submodules: recursive 158 | 159 | - name: Run test cases 160 | env: 161 | JF_TOKEN: ${{ steps.jf.outputs.oidc-token }} 162 | JF_USERNAME: ${{ steps.jf.outputs.oidc-user }} 163 | run: .github/packaging/project/test/gha-test-main.sh ${{ matrix.distro }} ${{ github.event.repository.name }} 164 | 165 | -------------------------------------------------------------------------------- /lib/utils/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | from distutils import debug 17 | from enum import Enum 18 | import traceback 19 | from sys import exit 20 | from typing import Optional 21 | 22 | 23 | from lib.view import terminal 24 | 25 | exit_code = 0 26 | 27 | 28 | def get_exit_code(): 29 | global exit_code 30 | return exit_code 31 | 32 | 33 | def set_exit_code(code): 34 | global exit_code 35 | exit_code = code 36 | 37 | 38 | class BaseLogger(logging.Logger, object): 39 | def _handle_exception(self, msg): 40 | if ( 41 | isinstance(msg, Exception) 42 | and not isinstance(msg, ShellException) 43 | and not isinstance(msg, ASProtocolError) 44 | and not isinstance(msg, ASInfoError) 45 | ): 46 | traceback.print_exc() 47 | 48 | def debug(self, msg, *args, **kwargs): 49 | kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 1 50 | 51 | super().debug(msg, *args, **kwargs) 52 | 53 | def error(self, msg, *args, **kwargs): 54 | super().error(msg, *args, **kwargs) 55 | 56 | if self.level <= logging.ERROR: 57 | self._handle_exception(msg) 58 | set_exit_code(2) 59 | 60 | def critical(self, msg, *args, **kwargs): 61 | super().critical(msg, *args, **kwargs) 62 | 63 | if self.level <= logging.CRITICAL: 64 | self._handle_exception(msg) 65 | set_exit_code(1) 66 | 67 | 68 | class _LogColors(Enum): 69 | red = "red" 70 | yellow = "yellow" 71 | 72 | 73 | class LogFormatter(logging.Formatter): 74 | def __init__(self, fmt="%(levelno)s: %(msg)s"): 75 | super().__init__(fmt=fmt) 76 | 77 | def _format_message(self, msg, level, color: Optional[_LogColors], args): 78 | try: 79 | message = str(msg) % args 80 | except Exception: 81 | message = str(msg) 82 | 83 | message = level + ": " + message 84 | 85 | if color == _LogColors.red: 86 | message = terminal.fg_red() + message + terminal.fg_clear() 87 | 88 | if color == _LogColors.yellow: 89 | message = terminal.fg_yellow() + message + terminal.fg_clear() 90 | 91 | return message 92 | 93 | def format(self, record: logging.LogRecord): 94 | if record.levelno == logging.DEBUG: 95 | path_split = record.pathname.split("lib") 96 | 97 | if len(path_split) > 1: 98 | path = "lib" + record.pathname.split("lib")[1] 99 | else: 100 | path = record.filename 101 | 102 | msg = self._format_message( 103 | f"{{{path}:{record.lineno}}} - {record.msg}", 104 | "DEBUG", 105 | None, 106 | record.args, 107 | ) 108 | 109 | if record.exc_info: 110 | exc = traceback.format_exception(*record.exc_info) 111 | for line in exc: 112 | for subline in line.split("\n"): 113 | if subline: 114 | msg += "\n" + self._format_message( 115 | f"{{{path}:{record.lineno}}} - {subline}", 116 | "DEBUG", 117 | None, 118 | record.args, 119 | ) 120 | msg += "\n" + self._format_message( 121 | f"{{{path}:{record.lineno}}} -", 122 | "DEBUG", 123 | None, 124 | record.args, 125 | ) 126 | 127 | return msg 128 | 129 | elif record.levelno == logging.INFO: 130 | return self._format_message(record.msg, "INFO", None, record.args) 131 | elif record.levelno == logging.WARNING: 132 | return self._format_message( 133 | record.msg, "WARNING", _LogColors.yellow, record.args 134 | ) 135 | elif record.levelno == logging.ERROR: 136 | return self._format_message( 137 | record.msg, "ERROR", _LogColors.red, record.args 138 | ) 139 | elif record.levelno == logging.CRITICAL: 140 | return self._format_message( 141 | record.msg, "ERROR", _LogColors.red, record.args 142 | ) 143 | 144 | formatter = logging.Formatter(self._style._fmt) 145 | return formatter.format(record) 146 | 147 | 148 | logging.setLoggerClass(BaseLogger) 149 | logger = logging.getLogger("lib") 150 | logger.propagate = False 151 | logger.setLevel( 152 | logging.WARNING 153 | ) # This only allows WARNING and above to be logged to handlers 154 | stderr_log_handler = logging.StreamHandler() 155 | stderr_log_handler.setLevel( 156 | logging.WARNING 157 | ) # This only allows WARNING and above to be logged to stderr. 158 | stderr_log_handler.setFormatter(LogFormatter()) 159 | logger.addHandler(stderr_log_handler) 160 | 161 | 162 | # must be imported after logger instantiation 163 | from lib.base_controller import ( # noqa: E402 - suppress flake warning 164 | ShellException, 165 | ) 166 | from lib.live_cluster.client import ( # noqa: E402 - suppress flake warning 167 | ASProtocolError, 168 | ASInfoError, 169 | ) 170 | -------------------------------------------------------------------------------- /test/unit/utils/test_lookup_dict.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import unittest 16 | 17 | from lib.utils.lookup_dict import PrefixDict, LookupDict 18 | 19 | 20 | class PrefixDictTest(unittest.TestCase): 21 | def setUp(self): 22 | self.test_dict = t = PrefixDict() 23 | 24 | t["u01.citrusleaf.local"] = 1 25 | t["u02.citrusleaf.local"] = 2 26 | t["u11.citrusleaf.local"] = 3 27 | t["u12"] = 4 28 | 29 | def test_len(self): 30 | self.assertEqual(len(self.test_dict), 4) 31 | 32 | def test_delete(self): 33 | def helper(prefix): 34 | del self.test_dict[prefix] 35 | 36 | helper("u12") 37 | self.assertEqual(len(self.test_dict), 3) 38 | 39 | self.assertRaises(KeyError, helper, "u") 40 | self.assertRaises(KeyError, helper, "asdf") 41 | 42 | value = self.test_dict.remove("u01") 43 | self.assertEqual(value, 1) 44 | 45 | def test_get(self): 46 | values = self.test_dict["u0"] 47 | self.assertTrue(1 in values) 48 | self.assertTrue(2 in values) 49 | self.assertRaises(KeyError, self.test_dict.__getitem__, "asdf") 50 | 51 | def test_getkey(self): 52 | keys = self.test_dict.get_key("u0") 53 | self.assertTrue("u01.citrusleaf.local" in keys) 54 | self.assertTrue("u02.citrusleaf.local" in keys) 55 | self.assertEqual(len(keys), 2) 56 | self.assertRaises(KeyError, self.test_dict.get_key, "asdf") 57 | 58 | def test_keys(self): 59 | keys = list(self.test_dict.keys()) 60 | self.assertTrue(len(keys), 4) 61 | self.assertTrue("u01.citrusleaf.local" in keys) 62 | self.assertTrue("u02.citrusleaf.local" in keys) 63 | 64 | def test_contains(self): 65 | self.assertTrue("u01.citrusleaf.local" in self.test_dict) 66 | self.assertFalse("asdf" in self.test_dict) 67 | 68 | def test_get_prefix(self): 69 | prefix = self.test_dict.get_prefix("u01") 70 | self.assertEqual(prefix, "u01") 71 | 72 | prefix = self.test_dict.get_prefix("u01.cit") 73 | self.assertEqual(prefix, "u01") 74 | 75 | self.assertRaises(KeyError, self.test_dict.get_prefix, "asdf") 76 | 77 | def test_setitem(self): 78 | self.test_dict["u33"] = 5 79 | self.assertEqual(self.test_dict["u33"][0], 5) 80 | 81 | self.test_dict["u01"] = 10 82 | self.assertTrue(1 in self.test_dict["u01"]) 83 | self.assertTrue(10 in self.test_dict["u01"]) 84 | 85 | 86 | class LookupDictTest(unittest.TestCase): 87 | def setUp(self): 88 | self.test_dict = t = LookupDict() 89 | 90 | t["192.168.0.11"] = 1 91 | t["192.168.0.12"] = 2 92 | t["192.168.0.21"] = 3 93 | t["192.168.0.22"] = 4 94 | 95 | def test_len(self): 96 | self.assertEqual(len(self.test_dict), 4) 97 | 98 | def test_delete(self): 99 | def helper(prefix): 100 | del self.test_dict[prefix] 101 | 102 | helper("11") 103 | self.assertEqual(len(self.test_dict), 3) 104 | 105 | self.assertRaises(KeyError, helper, "192") 106 | self.assertRaises(KeyError, helper, "asdf") 107 | 108 | value = self.test_dict.remove("21") 109 | self.assertEqual(value, 3) 110 | 111 | def test_get(self): 112 | values = self.test_dict["1"] 113 | 114 | self.assertTrue(1 in values) 115 | self.assertTrue(3 in values) 116 | self.assertRaises(KeyError, self.test_dict.__getitem__, "asdf") 117 | 118 | def test_getkey(self): 119 | keys = self.test_dict.get_key("1") 120 | self.assertTrue("192.168.0.11" in keys) 121 | self.assertTrue("192.168.0.21" in keys) 122 | self.assertEqual(len(keys), 4) 123 | self.assertRaises(KeyError, self.test_dict.get_key, "asdf") 124 | 125 | def test_keys(self): 126 | keys = list(self.test_dict.keys()) 127 | self.assertTrue(len(keys), 4) 128 | self.assertTrue("192.168.0.11" in keys) 129 | self.assertTrue("192.168.0.12" in keys) 130 | 131 | def test_contains(self): 132 | self.assertTrue("192.168.0.11" in self.test_dict) 133 | self.assertFalse("asdf" in self.test_dict) 134 | 135 | def test_setitem(self): 136 | self.test_dict["192.168.0.31"] = 5 137 | self.assertEqual(self.test_dict["31"][0], 5) 138 | 139 | self.test_dict["11"] = 10 140 | self.assertTrue(1 in self.test_dict["11"]) 141 | self.assertTrue(10 in self.test_dict["11"]) 142 | 143 | def test_getshortname(self): 144 | short_name = self.test_dict.get_shortname("192.168.0.11") 145 | self.assertEqual(short_name, "192.168.0.11") 146 | 147 | self.test_dict["u01.test.com.b-india"] = 5 148 | self.test_dict["u02.test.com.usa"] = 5 149 | self.test_dict["u03.test.com.aus"] = 5 150 | self.test_dict["u04.test.com.a-india"] = 5 151 | self.test_dict["u05.test.com.c-india"] = 5 152 | 153 | short_name = self.test_dict.get_shortname("u01.test.com.b-india") 154 | self.assertEqual(short_name, "u01...b-india") 155 | 156 | short_name = self.test_dict.get_shortname("u02.test.com.usa") 157 | self.assertEqual(short_name, "u02...sa") 158 | 159 | self.assertRaises(KeyError, self.test_dict.get_shortname, "asdf") 160 | -------------------------------------------------------------------------------- /lib/utils/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2025 Aerospike, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from enum import Enum 15 | import os 16 | from typing import Literal 17 | 18 | ADMIN_HOME = os.path.expanduser("~") + "/.aerospike/" 19 | 20 | # Admin port visual cue message 21 | ADMIN_PORT_VISUAL_CUE_MSG = ( 22 | "Connected via admin port. Note: Cluster discovery is disabled for admin ports." 23 | ) 24 | 25 | # For "manage config". Directory is relative to live_cluster/client 26 | # This variable should match what is reflected in pyinstaller-build.spec 27 | CONFIG_SCHEMAS_HOME = "schemas/json/aerospike" 28 | 29 | CONFIG_LOGGING = "logging" 30 | CONFIG_SERVICE = "service" 31 | CONFIG_SECURITY = "security" 32 | CONFIG_NETWORK = "network" 33 | CONFIG_NAMESPACE = "namespace" 34 | CONFIG_SET = "set" 35 | CONFIG_XDR = "xdr" 36 | CONFIG_DC = "dc" 37 | CONFIG_XDR_NS = "xdr_ns" 38 | CONFIG_XDR_FILTER = "xdr_filter" 39 | CONFIG_RACK_IDS = "rack-ids" 40 | CONFIG_ROSTER = "roster" 41 | CONFIG_RACKS = "racks" 42 | 43 | STAT_SERVICE = "service" 44 | STAT_SETS = "set" 45 | STAT_NAMESPACE = "namespace" 46 | STAT_XDR = "xdr" 47 | STAT_DC = "dc" 48 | STAT_XDR_NS = "xdr_ns" 49 | STAT_BINS = "bin" 50 | STAT_SINDEX = "sindex" 51 | 52 | SUMMARY_SERVICE = "service" 53 | SUMMARY_NETWORK = "network" 54 | SUMMARY_NAMESPACE = "namespace" 55 | SUMMARY_SETS = "sets" 56 | SUMMARY_XDR = "xdr" 57 | SUMMARY_DC = "dc" 58 | SUMMARY_SINDEX = "sindex" 59 | 60 | METADATA_JOBS = "jobs" 61 | METADATA_PRACTICES = "best_practices" 62 | METADATA_UDF = "udf" 63 | METADATA_UDF_CONTENT = "udf_content" 64 | 65 | ADMIN_ROLES = "roles" 66 | ADMIN_USERS = "users" 67 | 68 | 69 | SHOW_RESULT_KEY = "show_result" 70 | COUNT_RESULT_KEY = "count_result" 71 | TOTAL_ROW_HEADER = "total" 72 | END_ROW_KEY = "End" 73 | 74 | DT_FMT = "%b %d %Y %H:%M:%S" 75 | 76 | CLUSTER_FILE = 0 77 | SERVER_FILE = 1 78 | SYSTEM_FILE = 2 79 | JSON_FILE = 3 80 | 81 | COLLECTINFO_SEPERATOR = "\n====ASCOLLECTINFO====\n" 82 | COLLECTINFO_PROGRESS_MSG = "Data collection for %s%s in progress..." 83 | 84 | MRT_SET = "": BinaryOperation(">").operate, 40 | "<": BinaryOperation("<").operate, 41 | ">=": BinaryOperation(">=").operate, 42 | "<=": BinaryOperation("<=").operate, 43 | "==": BinaryOperation("==").operate, 44 | "!=": BinaryOperation("!=").operate, 45 | "%%": BinaryOperation("%%").operate, 46 | "&&": BinaryOperation("AND").operate, 47 | "||": BinaryOperation("OR").operate, 48 | "IN": BinaryOperation("IN").operate, 49 | "AND": AggOperation("AND").operate, 50 | "OR": AggOperation("OR").operate, 51 | "SUM": AggOperation("+").operate, 52 | "AVG": AggOperation("AVG").operate, 53 | "EQUAL": AggOperation("==").operate, 54 | "MAX": AggOperation("MAX").operate, 55 | "MIN": AggOperation("MIN").operate, 56 | "PRODUCT": AggOperation("*").operate, 57 | "COUNT": AggOperation("COUNT").operate, 58 | "COUNT_ALL": AggOperation("COUNT_ALL").operate, 59 | "FIRST": AggOperation("FIRST").operate, 60 | "VALUE_UNIFORM": AggOperation("VALUE_UNIFORM").operate, 61 | "DIFF": ComplexOperation("DIFF").operate, 62 | "SD_ANOMALY": ComplexOperation("SD_ANOMALY").operate, 63 | "NO_MATCH": ComplexOperation("NO_MATCH").operate, 64 | "APPLY_TO_ANY": ApplyOperation("ANY").operate, 65 | "APPLY_TO_ALL": ApplyOperation("ALL").operate, 66 | "SPLIT": SimpleOperation("SPLIT").operate, 67 | "UNIQUE": SimpleOperation("UNIQUE").operate, 68 | } 69 | 70 | assert_op_list = {"ASSERT": AssertDetailOperation("==").operate} 71 | 72 | 73 | def do_operation( 74 | op=None, 75 | arg1=None, 76 | arg2=None, 77 | group_by=None, 78 | result_comp_op=None, 79 | result_comp_val=None, 80 | on_common_only=False, 81 | save_param=None, 82 | ): 83 | if op in op_list: 84 | return op_list[op]( 85 | arg1, 86 | arg2, 87 | group_by, 88 | result_comp_op, 89 | result_comp_val, 90 | on_common_only=on_common_only, 91 | save_param=save_param, 92 | ) 93 | 94 | return None 95 | 96 | 97 | def select_keys( 98 | data={}, select_keys=[], select_from_keys=[], ignore_keys=[], save_param=None 99 | ): 100 | if not data or not isinstance(data, dict): 101 | raise HealthException("Wrong Input Data for select operation.") 102 | 103 | if not select_keys: 104 | raise HealthException("No key provided for select operation.") 105 | 106 | if not select_from_keys: 107 | select_from_keys = [] 108 | 109 | if not select_from_keys or ( 110 | select_from_keys[0] != "ALL" 111 | and not select_from_keys[0].startswith(SNAPSHOT_KEY_PREFIX) 112 | ): 113 | select_from_keys.insert(0, util.create_snapshot_key(len(data.keys()) - 1)) 114 | elif select_from_keys[0].startswith(SNAPSHOT_KEY_PREFIX): 115 | select_from_keys[0] = util.create_snapshot_key( 116 | int(re.search(SNAPSHOT_KEY_PATTERN, select_from_keys[0]).group(1)) 117 | ) 118 | 119 | config_param = False 120 | if "CONFIG" in select_from_keys: 121 | config_param = True 122 | 123 | result = select_keys_from_dict( 124 | data=data, 125 | keys=select_keys, 126 | from_keys=select_from_keys, 127 | ignore_keys=ignore_keys, 128 | save_param=save_param, 129 | config_param=config_param, 130 | ) 131 | 132 | if not result: 133 | raise HealthException( 134 | "Wrong input for select operation, Nothing matches with input keys." 135 | ) 136 | 137 | return result 138 | 139 | 140 | def do_assert( 141 | op=None, 142 | data={}, 143 | check_val=util.create_health_internal_tuple(True, []), 144 | error=None, 145 | category=None, 146 | level=None, 147 | description=None, 148 | success_msg=None, 149 | ): 150 | if op in assert_op_list: 151 | return assert_op_list[op]( 152 | data, check_val, error, category, level, description, success_msg 153 | ) 154 | 155 | return None 156 | 157 | 158 | def do_assert_if_check(op=None, arg1=None, arg2=None): 159 | """ 160 | Function takes operands and operator for assert if condition and evaluate that conditions. 161 | Returns boolean to indicate need to skip assert or not, and argument to OR with actual assert input to filter keys 162 | """ 163 | 164 | if arg1 is None or (not arg1 and arg1 != 0 and arg1 is not False): 165 | return False, None 166 | 167 | if op and arg2: 168 | arg1 = do_operation(op=op, arg1=arg1, arg2=arg2) 169 | 170 | # return filter argument should be in boolean form, True for key to skip and False for key to check 171 | return not is_data_true(arg1), do_operation(op="==", arg1=arg1, arg2=(False, [])) 172 | 173 | 174 | def is_data_true(data): 175 | """ 176 | Function takes dictionary and finds out, it contains any valid non-empty, no False, no zero value 177 | """ 178 | 179 | if not data: 180 | return False 181 | 182 | if not isinstance(data, dict): 183 | if not util.get_value_from_health_internal_tuple(data): 184 | return False 185 | return True 186 | 187 | for _k in data: 188 | if is_data_true(data[_k]): 189 | return True 190 | 191 | return False 192 | -------------------------------------------------------------------------------- /asinfo/asinfo.txt: -------------------------------------------------------------------------------- 1 | The asinfo script allows access to the services port on the server. 2 | This allows the user to query the server for any statistics available, 3 | and set any dynamic configuration values. This list is extensive, 4 | please contact support for a full list. 5 | 6 | Usage: asinfo [OPTIONS] 7 | --------------------------------------------------------------------------------------- 8 | 9 | -V --version Show the version of asinfo and exit 10 | -E --help Show program usage. 11 | -v --value Fetch single value (Default: all). 12 | -l --line-separator Print in separate lines (Default: False). 13 | --timeout=value Set timeout value in seconds. 14 | TLS connection does not support timeout. Default: 5 seconds 15 | 16 | 17 | Configuration File Allowed Options 18 | ---------------------------------- 19 | 20 | [cluster] 21 | -h HOST, --host=HOST 22 | HOST is "[:][:],..." 23 | Server seed hostnames or IP addresses. The tlsname is 24 | only used when connecting with a secure TLS enabled 25 | server. Default: localhost:3000 26 | Examples: 27 | host1 28 | host1:3000,host2:3000 29 | 192.168.1.10:cert1:3000,192.168.1.20:cert2:3000 30 | -p PORT, --port=PORT 31 | Server default port. Default: 3000 32 | -U USER, --user=USER 33 | User name used to authenticate with cluster. Default: none 34 | -P, --password 35 | Password used to authenticate with cluster. Default: none 36 | User will be prompted on command line if -P specified and no 37 | password is given. 38 | --auth=AUTHENTICATION_MODE 39 | Authentication mode. Values: ['EXTERNAL', 'EXTERNAL_INSECURE', 'INTERNAL', 'PKI']. 40 | Default: INTERNAL 41 | --tls-enable 42 | Enable TLS on connections. By default TLS is disabled. 43 | -t TLS_NAME, --tls-name=TLS_NAME 44 | Specify host tlsname. 45 | --tls-cafile=TLS_CAFILE 46 | Path to a trusted CA certificate file. 47 | --tls-capath=TLS_CAPATH 48 | Path to a directory of trusted CA certificates. 49 | --tls-protocols=TLS_PROTOCOLS 50 | Set the TLS protocol selection criteria. This format 51 | is the same as Apache's SSLProtocol documented at http 52 | s://httpd.apache.org/docs/current/mod/mod_ssl.html#ssl 53 | protocol . If not specified the asinfo will use '-all 54 | +TLSv1.2' if has support for TLSv1.2,otherwise it will 55 | be '-all +TLSv1'. 56 | --tls-cipher-suite=TLS_CIPHER_SUITE 57 | Set the TLS cipher selection criteria. The format is 58 | the same as Open_sSL's Cipher List Format documented 59 | at https://www.openssl.org/docs/man1.0.1/apps/ciphers. 60 | html 61 | --tls-keyfile=TLS_KEYFILE 62 | Path to the key for mutual authentication (if 63 | Aerospike Cluster is supporting it). 64 | --tls-keyfile-password=password 65 | Password to load protected tls-keyfile. 66 | It can be one of the following: 67 | 1) Environment variable: 'env:' 68 | 2) File: 'file:' 69 | 3) String: 'PASSWORD' 70 | Default: none 71 | User will be prompted on command line if --tls-keyfile-password specified and no 72 | password is given. 73 | --tls-certfile=TLS_CERTFILE 74 | Path to the chain file for mutual authentication (if 75 | Aerospike Cluster is supporting it). 76 | --tls-cert-blacklist 77 | Path to a certificate blacklist file. The file should 78 | contain one line for each blacklisted certificate. 79 | Each line starts with the certificate serial number 80 | expressed in hex. Each entry may optionally specify 81 | the issuer name of the certificate (serial numbers are 82 | only required to be unique per issuer).Example: 83 | 867EC87482B2 84 | /C=US/ST=CA/O=Acme/OU=Engineering/CN=TestChainCA 85 | --tls-crl-check Enable CRL checking for leaf certificate. An error 86 | occurs if a valid CRL files cannot be found in 87 | tls_capath. 88 | --tls-crl-check-all Enable CRL checking for entire certificate chain. An 89 | error occurs if a valid CRL files cannot be found in 90 | tls_capath. 91 | 92 | 93 | 94 | 95 | Default configuration files are read from the following files in the given order: 96 | /etc/aerospike/astools.conf ~/.aerospike/astools.conf 97 | The following sections are read: (cluster include) 98 | The following options effect configuration file behavior 99 | 100 | --no-config-file 101 | Do not read any config file. Default: disabled 102 | --instance 103 | Section with these instance is read. e.g in case instance 104 | `a` is specified section cluster_a is read. 105 | --config-file 106 | Read this file after default configuration file. 107 | --only-config-file 108 | Read only this configuration file. 109 | 110 | 111 | Here are some basic commands that are often used: 112 | 113 | This command lists all the nodes listed as available for this node 114 | in it's services list 115 | 116 | asinfo -v 'services' 117 | 118 | This queries for information on the namespace named "users": 119 | 120 | asinfo -v 'namespace/users' 121 | 122 | This command queries for the same information as above, 123 | but on a remote node specified by -h and on port 3000 specified by -p: 124 | 125 | asinfo -h 192.168.120.101 -p 3000 -v 'namespace/users' 126 | 127 | This command queries for the same information as above, 128 | but on a remote node and tls connection enabled: 129 | 130 | asinfo -h 192.168.120.101 -p 3000 -t t3.t-cluster.aerospike.com --tls_enable --tls_cafile ~/x509_certificates/Platinum/cacert.pem -v 'namespace/users' 131 | 132 | --------------------------------------------------------------------------------