greenlake-activate-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- greenlake_activate_cli-0.1.0/PKG-INFO +188 -0
- greenlake_activate_cli-0.1.0/README.md +177 -0
- greenlake_activate_cli-0.1.0/pyproject.toml +27 -0
- greenlake_activate_cli-0.1.0/setup.cfg +4 -0
- greenlake_activate_cli-0.1.0/src/glcli/__init__.py +0 -0
- greenlake_activate_cli-0.1.0/src/glcli/activate_login.py +67 -0
- greenlake_activate_cli-0.1.0/src/glcli/cli.py +124 -0
- greenlake_activate_cli-0.1.0/src/glcli/data_parsing.py +45 -0
- greenlake_activate_cli-0.1.0/src/glcli/display_data.py +14 -0
- greenlake_activate_cli-0.1.0/src/glcli/query_folder.py +74 -0
- greenlake_activate_cli-0.1.0/src/glcli/query_inventory.py +136 -0
- greenlake_activate_cli-0.1.0/src/greenlake_activate_cli.egg-info/PKG-INFO +188 -0
- greenlake_activate_cli-0.1.0/src/greenlake_activate_cli.egg-info/SOURCES.txt +20 -0
- greenlake_activate_cli-0.1.0/src/greenlake_activate_cli.egg-info/dependency_links.txt +1 -0
- greenlake_activate_cli-0.1.0/src/greenlake_activate_cli.egg-info/entry_points.txt +2 -0
- greenlake_activate_cli-0.1.0/src/greenlake_activate_cli.egg-info/requires.txt +4 -0
- greenlake_activate_cli-0.1.0/src/greenlake_activate_cli.egg-info/top_level.txt +1 -0
- greenlake_activate_cli-0.1.0/tests/test_activate_login.py +62 -0
- greenlake_activate_cli-0.1.0/tests/test_cli.py +173 -0
- greenlake_activate_cli-0.1.0/tests/test_data_parsing.py +15 -0
- greenlake_activate_cli-0.1.0/tests/test_query_folder.py +67 -0
- greenlake_activate_cli-0.1.0/tests/test_query_inventory.py +73 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: greenlake-activate-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI tool used to interact with HPE GreenLake Activate
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: python-dotenv>=1.2.3
|
|
8
|
+
Requires-Dist: requests>=2.34.2
|
|
9
|
+
Requires-Dist: rich>=15.0.0
|
|
10
|
+
Requires-Dist: typer>=0.27.2
|
|
11
|
+
|
|
12
|
+
# GreenLake Activate CLI
|
|
13
|
+
|
|
14
|
+
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface.
|
|
15
|
+
|
|
16
|
+
# Setup
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.14 or newer
|
|
21
|
+
- An HPE GreenLake Activate API credential
|
|
22
|
+
- Network access to `activate.arubanetworks.com`
|
|
23
|
+
|
|
24
|
+
The CLI uses the following Python packages, which are installed automatically:
|
|
25
|
+
|
|
26
|
+
- Typer
|
|
27
|
+
- Requests
|
|
28
|
+
- Rich
|
|
29
|
+
- Python Dotenv
|
|
30
|
+
|
|
31
|
+
## Install With uv
|
|
32
|
+
|
|
33
|
+
[Install uv](https://docs.astral.sh/uv/getting-started/installation/) if it is not
|
|
34
|
+
already available. For a published release, install the CLI as a global command:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
uv tool install greenlake-activate-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This installs the CLI in an isolated environment and makes `glcli` available from
|
|
41
|
+
any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv tool update-shell
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Restart your shell, then verify the installation:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
glcli --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To upgrade or remove the installed CLI:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
uv tool upgrade greenlake-activate-cli
|
|
57
|
+
uv tool uninstall greenlake-activate-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`pipx` is an alternative isolated installer:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pipx install greenlake-activate-cli
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For development from a source checkout, clone the repository and run:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
git clone <repository-url>
|
|
70
|
+
cd greenlake_activate_cli
|
|
71
|
+
uv sync
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The `uv sync` command creates the project environment, installs the application and
|
|
75
|
+
its dependencies, and makes the `glcli` command available through `uv run`:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
uv run glcli --help
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
To install the development dependencies, including the test suite, use:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
uv sync --dev
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Install With Python and pip
|
|
88
|
+
|
|
89
|
+
Create a virtual environment, activate it, and install the project from the cloned
|
|
90
|
+
repository:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
python3.14 -m venv .venv
|
|
94
|
+
. .venv/bin/activate
|
|
95
|
+
python -m pip install --upgrade pip
|
|
96
|
+
python -m pip install .
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The `glcli` command is then available directly:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
glcli --help
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configure Credentials
|
|
106
|
+
|
|
107
|
+
The recommended configuration file is:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
mkdir -p ~/.config/greenlake-activate-cli
|
|
111
|
+
printf 'CREDENTIAL_1=your_api_key_here\n' \
|
|
112
|
+
> ~/.config/greenlake-activate-cli/.env
|
|
113
|
+
chmod 600 ~/.config/greenlake-activate-cli/.env
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The CLI also accepts the environment variable directly:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
export CREDENTIAL_1="your_api_key_here"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Credential sources are checked in this order:
|
|
123
|
+
|
|
124
|
+
1. A non-empty `CREDENTIAL_1` environment variable.
|
|
125
|
+
2. `$XDG_CONFIG_HOME/greenlake-activate-cli/.env`, when `XDG_CONFIG_HOME` is set.
|
|
126
|
+
3. `~/.config/greenlake-activate-cli/.env`.
|
|
127
|
+
4. `.env` in the current directory, retained for development and backward compatibility.
|
|
128
|
+
|
|
129
|
+
The configuration file format is:
|
|
130
|
+
|
|
131
|
+
```plain
|
|
132
|
+
CREDENTIAL_1=<your_api_key_here>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Do not commit `.env` or share the credential. If no source contains `CREDENTIAL_1`,
|
|
136
|
+
the CLI reports the supported locations and exits before sending an inventory query.
|
|
137
|
+
|
|
138
|
+
# Usage
|
|
139
|
+
|
|
140
|
+
Query inventory by serial number:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
glcli query serial PHWLKAS02 PHWLKAS03
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Query inventory by MAC address:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
glcli query mac aa:bb:cc:00:11:22 dd:ee:ff:33:44:55
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Query inventory by folder ID or folder name:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
glcli query folder 5297450
|
|
156
|
+
glcli query folder SiteA-South
|
|
157
|
+
glcli query folder SiteA-South 5389522
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Folder names are matched case-insensitively and exactly. Numeric values are treated
|
|
161
|
+
as folder IDs. Folder names are resolved through Activate before the inventory query;
|
|
162
|
+
unknown or ambiguous names are rejected. Folder queries currently accept positional
|
|
163
|
+
values only and do not support `--file`.
|
|
164
|
+
|
|
165
|
+
Serial numbers and MAC addresses can also be loaded from CSV or newline-delimited files:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
glcli query serial --file serials.csv
|
|
169
|
+
glcli query mac --file mac_addresses.csv
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The file reader uses the first column. Serial files may use a `serial`, `serialNumber`,
|
|
173
|
+
`serial_number`, `serial number`, or `sn` header.
|
|
174
|
+
|
|
175
|
+
Use `--verbose` for diagnostic logging:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
glcli --verbose query serial PHWLKAS02
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Exit codes are `0` for a complete result, `1` when no devices are returned, and
|
|
182
|
+
`2` when at least one requested identifier is missing.
|
|
183
|
+
|
|
184
|
+
Run the offline tests with:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
uv run pytest
|
|
188
|
+
```
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# GreenLake Activate CLI
|
|
2
|
+
|
|
3
|
+
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface.
|
|
4
|
+
|
|
5
|
+
# Setup
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.14 or newer
|
|
10
|
+
- An HPE GreenLake Activate API credential
|
|
11
|
+
- Network access to `activate.arubanetworks.com`
|
|
12
|
+
|
|
13
|
+
The CLI uses the following Python packages, which are installed automatically:
|
|
14
|
+
|
|
15
|
+
- Typer
|
|
16
|
+
- Requests
|
|
17
|
+
- Rich
|
|
18
|
+
- Python Dotenv
|
|
19
|
+
|
|
20
|
+
## Install With uv
|
|
21
|
+
|
|
22
|
+
[Install uv](https://docs.astral.sh/uv/getting-started/installation/) if it is not
|
|
23
|
+
already available. For a published release, install the CLI as a global command:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uv tool install greenlake-activate-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This installs the CLI in an isolated environment and makes `glcli` available from
|
|
30
|
+
any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uv tool update-shell
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Restart your shell, then verify the installation:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
glcli --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To upgrade or remove the installed CLI:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uv tool upgrade greenlake-activate-cli
|
|
46
|
+
uv tool uninstall greenlake-activate-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`pipx` is an alternative isolated installer:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pipx install greenlake-activate-cli
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For development from a source checkout, clone the repository and run:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
git clone <repository-url>
|
|
59
|
+
cd greenlake_activate_cli
|
|
60
|
+
uv sync
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `uv sync` command creates the project environment, installs the application and
|
|
64
|
+
its dependencies, and makes the `glcli` command available through `uv run`:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
uv run glcli --help
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To install the development dependencies, including the test suite, use:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
uv sync --dev
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Install With Python and pip
|
|
77
|
+
|
|
78
|
+
Create a virtual environment, activate it, and install the project from the cloned
|
|
79
|
+
repository:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
python3.14 -m venv .venv
|
|
83
|
+
. .venv/bin/activate
|
|
84
|
+
python -m pip install --upgrade pip
|
|
85
|
+
python -m pip install .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `glcli` command is then available directly:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
glcli --help
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configure Credentials
|
|
95
|
+
|
|
96
|
+
The recommended configuration file is:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
mkdir -p ~/.config/greenlake-activate-cli
|
|
100
|
+
printf 'CREDENTIAL_1=your_api_key_here\n' \
|
|
101
|
+
> ~/.config/greenlake-activate-cli/.env
|
|
102
|
+
chmod 600 ~/.config/greenlake-activate-cli/.env
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The CLI also accepts the environment variable directly:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
export CREDENTIAL_1="your_api_key_here"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Credential sources are checked in this order:
|
|
112
|
+
|
|
113
|
+
1. A non-empty `CREDENTIAL_1` environment variable.
|
|
114
|
+
2. `$XDG_CONFIG_HOME/greenlake-activate-cli/.env`, when `XDG_CONFIG_HOME` is set.
|
|
115
|
+
3. `~/.config/greenlake-activate-cli/.env`.
|
|
116
|
+
4. `.env` in the current directory, retained for development and backward compatibility.
|
|
117
|
+
|
|
118
|
+
The configuration file format is:
|
|
119
|
+
|
|
120
|
+
```plain
|
|
121
|
+
CREDENTIAL_1=<your_api_key_here>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Do not commit `.env` or share the credential. If no source contains `CREDENTIAL_1`,
|
|
125
|
+
the CLI reports the supported locations and exits before sending an inventory query.
|
|
126
|
+
|
|
127
|
+
# Usage
|
|
128
|
+
|
|
129
|
+
Query inventory by serial number:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
glcli query serial PHWLKAS02 PHWLKAS03
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Query inventory by MAC address:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
glcli query mac aa:bb:cc:00:11:22 dd:ee:ff:33:44:55
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Query inventory by folder ID or folder name:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
glcli query folder 5297450
|
|
145
|
+
glcli query folder SiteA-South
|
|
146
|
+
glcli query folder SiteA-South 5389522
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Folder names are matched case-insensitively and exactly. Numeric values are treated
|
|
150
|
+
as folder IDs. Folder names are resolved through Activate before the inventory query;
|
|
151
|
+
unknown or ambiguous names are rejected. Folder queries currently accept positional
|
|
152
|
+
values only and do not support `--file`.
|
|
153
|
+
|
|
154
|
+
Serial numbers and MAC addresses can also be loaded from CSV or newline-delimited files:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
glcli query serial --file serials.csv
|
|
158
|
+
glcli query mac --file mac_addresses.csv
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The file reader uses the first column. Serial files may use a `serial`, `serialNumber`,
|
|
162
|
+
`serial_number`, `serial number`, or `sn` header.
|
|
163
|
+
|
|
164
|
+
Use `--verbose` for diagnostic logging:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
glcli --verbose query serial PHWLKAS02
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Exit codes are `0` for a complete result, `1` when no devices are returned, and
|
|
171
|
+
`2` when at least one requested identifier is missing.
|
|
172
|
+
|
|
173
|
+
Run the offline tests with:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
uv run pytest
|
|
177
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "greenlake-activate-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A CLI tool used to interact with HPE GreenLake Activate"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"python-dotenv>=1.2.3",
|
|
9
|
+
"requests>=2.34.2",
|
|
10
|
+
"rich>=15.0.0",
|
|
11
|
+
"typer>=0.27.2",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
glcli = "glcli.cli:main"
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.0.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["setuptools>=61"]
|
|
24
|
+
build-backend = "setuptools.build_meta"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from dotenv import dotenv_values
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
REQUEST_TIMEOUT = 30
|
|
10
|
+
ENVIRONMENT_VARIABLE = "CREDENTIAL_1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def user_config_path() -> Path:
|
|
14
|
+
"""Return the user-level configuration path for the current platform."""
|
|
15
|
+
config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
16
|
+
if config_home:
|
|
17
|
+
return Path(config_home) / "greenlake-activate-cli" / ".env"
|
|
18
|
+
return Path.home() / ".config" / "greenlake-activate-cli" / ".env"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def credential_config_paths() -> tuple[Path, Path]:
|
|
22
|
+
"""Return user-level and project-local credential file paths."""
|
|
23
|
+
return user_config_path(), Path.cwd() / ".env"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_credentials() -> str:
|
|
27
|
+
"""Load credentials using environment, user config, then local config."""
|
|
28
|
+
credential = os.environ.get(ENVIRONMENT_VARIABLE, "").strip()
|
|
29
|
+
if credential:
|
|
30
|
+
return credential
|
|
31
|
+
|
|
32
|
+
user_path, local_path = credential_config_paths()
|
|
33
|
+
for path in (user_path, local_path):
|
|
34
|
+
if not path.is_file():
|
|
35
|
+
continue
|
|
36
|
+
credential = str(dotenv_values(path).get(ENVIRONMENT_VARIABLE) or "").strip()
|
|
37
|
+
if credential:
|
|
38
|
+
return credential
|
|
39
|
+
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
f"{ENVIRONMENT_VARIABLE} is not configured. Set it in the environment, "
|
|
42
|
+
f"or create {user_path} or {local_path} with {ENVIRONMENT_VARIABLE}=<token>."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def create_activate_session(credential_1:str) -> requests.Session:
|
|
46
|
+
"""Login and return session."""
|
|
47
|
+
if not credential_1:
|
|
48
|
+
raise RuntimeError("CREDENTIAL_1 is not configured")
|
|
49
|
+
|
|
50
|
+
login_url = "https://activate.arubanetworks.com/LOGIN"
|
|
51
|
+
login_data = {
|
|
52
|
+
'credential_0': "username",
|
|
53
|
+
'credential_1': credential_1
|
|
54
|
+
}
|
|
55
|
+
logger.debug("Attempting to authenticate to: %s", login_url)
|
|
56
|
+
session = requests.session()
|
|
57
|
+
response = session.post(login_url, data=login_data, timeout=REQUEST_TIMEOUT)
|
|
58
|
+
|
|
59
|
+
logger.debug("Login status code: %s", response.status_code)
|
|
60
|
+
if response.status_code != 200:
|
|
61
|
+
logger.error("Login failed: %s", response.text)
|
|
62
|
+
raise RuntimeError("Activate login failed")
|
|
63
|
+
|
|
64
|
+
logger.info("Authenticated to Activate")
|
|
65
|
+
logger.debug("Full login response: %s", response.text)
|
|
66
|
+
|
|
67
|
+
return session
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from glcli.activate_login import create_activate_session, load_credentials
|
|
6
|
+
from glcli.data_parsing import parse_inventory_response
|
|
7
|
+
from glcli.query_inventory import query_inventory, read_identifiers_from_file
|
|
8
|
+
from glcli.query_folder import resolve_folder_ids
|
|
9
|
+
from glcli.display_data import display_inventory_sn
|
|
10
|
+
from rich.logging import RichHandler
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
app = typer.Typer(help="Interact with HPE GreenLake Activate via CLI.")
|
|
14
|
+
query_app = typer.Typer(help="Query Activate inventory.")
|
|
15
|
+
app.add_typer(query_app, name="query")
|
|
16
|
+
|
|
17
|
+
def setup_logging(verbose: bool):
|
|
18
|
+
logging.basicConfig(
|
|
19
|
+
level=logging.DEBUG if verbose else logging.INFO,
|
|
20
|
+
format="%(levelname)s %(name)s: %(message)s" if verbose else "%(message)s",
|
|
21
|
+
handlers=[RichHandler(rich_tracebacks=True, show_path=verbose)],
|
|
22
|
+
)
|
|
23
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_credential() -> str:
|
|
27
|
+
try:
|
|
28
|
+
return load_credentials()
|
|
29
|
+
except RuntimeError as exc:
|
|
30
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
31
|
+
raise typer.Exit(code=1) from exc
|
|
32
|
+
|
|
33
|
+
def _query(identifier_type: str, identifiers: list[str], file: Path | None = None) -> None:
|
|
34
|
+
if file is not None:
|
|
35
|
+
identifiers = read_identifiers_from_file(file, identifier_type)
|
|
36
|
+
|
|
37
|
+
identifiers = [identifier.strip().upper() for identifier in identifiers if identifier.strip()]
|
|
38
|
+
if not identifiers:
|
|
39
|
+
raise typer.BadParameter("At least one identifier is required")
|
|
40
|
+
|
|
41
|
+
identifiers = list(dict.fromkeys(identifiers))
|
|
42
|
+
credential = _load_credential()
|
|
43
|
+
try:
|
|
44
|
+
session = create_activate_session(credential)
|
|
45
|
+
query_result, missing = query_inventory(session, identifier_type, identifiers)
|
|
46
|
+
finally:
|
|
47
|
+
if "session" in locals():
|
|
48
|
+
session.close()
|
|
49
|
+
|
|
50
|
+
extracted_data = parse_inventory_response(query_result)
|
|
51
|
+
|
|
52
|
+
if missing:
|
|
53
|
+
print(f"Not found: ({len(missing)}): {', '.join(missing)}")
|
|
54
|
+
|
|
55
|
+
display_inventory_sn(extracted_data)
|
|
56
|
+
|
|
57
|
+
if not extracted_data:
|
|
58
|
+
raise typer.Exit(code=1)
|
|
59
|
+
if missing:
|
|
60
|
+
raise typer.Exit(code=2)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _query_folder(values: list[str]) -> None:
|
|
64
|
+
values = [value.strip() for value in values if value.strip()]
|
|
65
|
+
if not values:
|
|
66
|
+
raise typer.BadParameter("At least one folder ID or name is required")
|
|
67
|
+
|
|
68
|
+
values = list(dict.fromkeys(values))
|
|
69
|
+
credential = _load_credential()
|
|
70
|
+
session = None
|
|
71
|
+
try:
|
|
72
|
+
session = create_activate_session(credential)
|
|
73
|
+
try:
|
|
74
|
+
folder_ids = resolve_folder_ids(session, values)
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
77
|
+
query_result, missing = query_inventory(session, "folder", folder_ids)
|
|
78
|
+
finally:
|
|
79
|
+
if session is not None:
|
|
80
|
+
session.close()
|
|
81
|
+
|
|
82
|
+
extracted_data = parse_inventory_response(query_result)
|
|
83
|
+
display_inventory_sn(extracted_data)
|
|
84
|
+
|
|
85
|
+
if not extracted_data:
|
|
86
|
+
raise typer.Exit(code=1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.callback()
|
|
90
|
+
def main_callback(
|
|
91
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debugging output to console."),
|
|
92
|
+
) -> None:
|
|
93
|
+
setup_logging(verbose)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@query_app.command("serial")
|
|
97
|
+
def query_serial(
|
|
98
|
+
serials: list[str] | None = typer.Argument(None, metavar="SERIAL"),
|
|
99
|
+
file: Path | None = typer.Option(None, "--file", "-f", help="CSV or text file containing serial numbers."),
|
|
100
|
+
) -> None:
|
|
101
|
+
_query("serial", serials or [], file)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@query_app.command("mac")
|
|
105
|
+
def query_mac(
|
|
106
|
+
macs: list[str] | None = typer.Argument(None, metavar="MAC"),
|
|
107
|
+
file: Path | None = typer.Option(None, "--file", "-f", help="CSV or text file containing MAC addresses."),
|
|
108
|
+
) -> None:
|
|
109
|
+
_query("mac", macs or [], file)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@query_app.command("folder")
|
|
113
|
+
def query_folder(
|
|
114
|
+
folders: list[str] = typer.Argument(..., metavar="FOLDER_ID_OR_NAME"),
|
|
115
|
+
) -> None:
|
|
116
|
+
_query_folder(folders)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main() -> None:
|
|
120
|
+
app()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
def parse_inventory_response(response:str) -> list[dict]:
|
|
7
|
+
"""Function to extract the data that I want to get from Activate."""
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
json_obj = json.loads(response)
|
|
11
|
+
except json.JSONDecodeError as exc:
|
|
12
|
+
raise ValueError("Inventory response is not valid JSON") from exc
|
|
13
|
+
|
|
14
|
+
if not isinstance(json_obj, dict):
|
|
15
|
+
raise ValueError("Inventory response must be a JSON object")
|
|
16
|
+
|
|
17
|
+
extracted_data: list[dict] = [] # each device is a dictionary
|
|
18
|
+
devices = json_obj.get("devices")
|
|
19
|
+
|
|
20
|
+
if isinstance(devices, list):
|
|
21
|
+
for device in devices:
|
|
22
|
+
if not isinstance(device, dict):
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
serial = device.get("serialNumber")
|
|
26
|
+
mac = device.get("mac")
|
|
27
|
+
status = device.get("status")
|
|
28
|
+
additional_data = device.get("additionalData")
|
|
29
|
+
if not isinstance(additional_data, dict):
|
|
30
|
+
additional_data = {}
|
|
31
|
+
folder = additional_data.get("folder")
|
|
32
|
+
folder_id = additional_data.get("folderId")
|
|
33
|
+
|
|
34
|
+
device_dict = {
|
|
35
|
+
"serial":serial,
|
|
36
|
+
"mac":mac,
|
|
37
|
+
"status":status,
|
|
38
|
+
"folder":folder,
|
|
39
|
+
"folderId":folder_id,
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
extracted_data.append(device_dict)
|
|
43
|
+
|
|
44
|
+
logger.debug("Extracted data:\n%s", extracted_data)
|
|
45
|
+
return extracted_data
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from rich import print
|
|
2
|
+
from rich.markup import escape
|
|
3
|
+
|
|
4
|
+
def display_inventory_sn(extracted_data:list[dict]):
|
|
5
|
+
"""Function to display output on the CLI when querying by SN"""
|
|
6
|
+
|
|
7
|
+
indent = " " * 5
|
|
8
|
+
for device in extracted_data:
|
|
9
|
+
print()
|
|
10
|
+
print(f"[yellow]Serial[/yellow]: [white]{escape(str(device['serial']))}[/white]")
|
|
11
|
+
print(f"{indent}[yellow]MAC Address[/yellow]: [white]{escape(str(device['mac']))}[/white]")
|
|
12
|
+
print(f"{indent}[yellow]Status[/yellow]: [white]{escape(str(device['status']))}[/white]")
|
|
13
|
+
print(f"{indent}[yellow]Folder[/yellow]: [white]{escape(str(device['folder']))}[/white]")
|
|
14
|
+
print(f"{indent}[yellow]Folder ID[/yellow]: [white]{escape(str(device['folderId']))}[/white]")
|