greenlake-activate-cli 0.2.2__tar.gz → 0.3.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.2.2 → greenlake_activate_cli-0.3.0}/PKG-INFO +53 -30
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/README.md +52 -29
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/pyproject.toml +1 -1
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/cli.py +63 -0
- greenlake_activate_cli-0.3.0/src/glcli/move_device.py +71 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/PKG-INFO +53 -30
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/SOURCES.txt +3 -0
- greenlake_activate_cli-0.3.0/tests/test_move_cli.py +90 -0
- greenlake_activate_cli-0.3.0/tests/test_move_device.py +62 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/setup.cfg +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/__init__.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/activate_login.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/data_parsing.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/display_data.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/query_folder.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/query_inventory.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/dependency_links.txt +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/entry_points.txt +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/requires.txt +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/top_level.txt +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_activate_login.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_cli.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_configure.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_data_parsing.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_query_folder.py +0 -0
- {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_query_inventory.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenlake-activate-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A CLI tool used to interact with HPE GreenLake Activate
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -11,23 +11,25 @@ Requires-Dist: typer>=0.27.2
|
|
|
11
11
|
|
|
12
12
|
# GreenLake Activate CLI
|
|
13
13
|
|
|
14
|
-
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface.
|
|
14
|
+
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface. This tool uses the available API calls that HPE GreenLake Activate exposes for manging devices
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Query Activate inventory by:
|
|
19
|
+
- device name
|
|
20
|
+
- mac address
|
|
21
|
+
- serial number
|
|
22
|
+
- folder name
|
|
23
|
+
- folder id
|
|
24
|
+
- Move devices between folders
|
|
25
|
+
- Generate CLI commands for allowlist entries (WIP)
|
|
17
26
|
|
|
18
27
|
## Requirements
|
|
19
28
|
|
|
20
|
-
- Python 3.
|
|
29
|
+
- Python 3.12 or newer
|
|
21
30
|
- An HPE GreenLake Activate API credential
|
|
22
31
|
- Network access to `activate.arubanetworks.com`
|
|
23
32
|
|
|
24
|
-
The CLI uses the following Python packages, which are installed automatically:
|
|
25
|
-
|
|
26
|
-
- Typer
|
|
27
|
-
- Requests
|
|
28
|
-
- Rich
|
|
29
|
-
- Python Dotenv
|
|
30
|
-
|
|
31
33
|
## Installation
|
|
32
34
|
|
|
33
35
|
I recommend installing this package with `uv`. [Install uv](https://docs.astral.sh/uv/getting-started/installation/) if it is not already available.
|
|
@@ -57,13 +59,6 @@ Verify the installation:
|
|
|
57
59
|
glcli --version
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
This installs the CLI in an isolated environment and makes `glcli` available from
|
|
61
|
-
any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
62
|
-
|
|
63
|
-
```sh
|
|
64
|
-
uv tool update-shell
|
|
65
|
-
```
|
|
66
|
-
|
|
67
62
|
To upgrade or remove the installed CLI:
|
|
68
63
|
|
|
69
64
|
```sh
|
|
@@ -73,23 +68,22 @@ uv tool uninstall greenlake-activate-cli
|
|
|
73
68
|
|
|
74
69
|
### Install With Python and pip
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
repository:
|
|
71
|
+
Creating a virtual environment and installing the package:
|
|
78
72
|
|
|
79
73
|
```sh
|
|
80
|
-
python3
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
python3 -m venv .venv
|
|
75
|
+
source .venv/bin/activate
|
|
76
|
+
python3 -m pip install --upgrade pip
|
|
77
|
+
python3 -m pip install greenlake-activate-cli
|
|
84
78
|
```
|
|
85
79
|
|
|
86
|
-
The `glcli` command is then available
|
|
80
|
+
The `glcli` command is then available from the venv:
|
|
87
81
|
|
|
88
82
|
```sh
|
|
89
83
|
glcli --version
|
|
90
84
|
```
|
|
91
85
|
|
|
92
|
-
##
|
|
86
|
+
## Configuration
|
|
93
87
|
|
|
94
88
|
Run the CLI `glcli configure` command.
|
|
95
89
|
|
|
@@ -97,7 +91,7 @@ Run the CLI `glcli configure` command.
|
|
|
97
91
|
glcli configure
|
|
98
92
|
```
|
|
99
93
|
|
|
100
|
-
You will be prompted to enter your GreenLake Activate API token. This text is hidden.
|
|
94
|
+
You will be prompted to enter (paste) in your GreenLake Activate API token. This text is hidden.
|
|
101
95
|
|
|
102
96
|
This command is essentially doing this:
|
|
103
97
|
|
|
@@ -148,6 +142,20 @@ glcli query folder 5297450
|
|
|
148
142
|
glcli query folder SiteA-South
|
|
149
143
|
```
|
|
150
144
|
|
|
145
|
+
Move a device by serial number or MAC address:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
glcli move PHWLKAS02 SiteA-South
|
|
149
|
+
glcli move aa:bb:cc:00:11:22 5297450
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Move multiple devices from a file:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
glcli move serials serials.csv SiteA-South
|
|
156
|
+
glcli move macs mac_addresses.csv 5297450
|
|
157
|
+
```
|
|
158
|
+
|
|
151
159
|
Serial numbers and MAC addresses can also be loaded from CSV or newline-delimited files:
|
|
152
160
|
|
|
153
161
|
```sh
|
|
@@ -155,9 +163,6 @@ glcli query serial --file serials.csv
|
|
|
155
163
|
glcli query mac --file mac_addresses.csv
|
|
156
164
|
```
|
|
157
165
|
|
|
158
|
-
The file reader uses the first column. Serial files may use a `serial`, `serialNumber`,
|
|
159
|
-
`serial_number`, `serial number`, or `sn` header.
|
|
160
|
-
|
|
161
166
|
Use `--verbose` for diagnostic logging:
|
|
162
167
|
|
|
163
168
|
```sh
|
|
@@ -166,3 +171,21 @@ glcli --verbose query serial PHWLKAS02
|
|
|
166
171
|
|
|
167
172
|
Exit codes are `0` for a complete result, `1` when no devices are returned, and
|
|
168
173
|
`2` when at least one requested identifier is missing.
|
|
174
|
+
|
|
175
|
+
## Troubleshooting
|
|
176
|
+
|
|
177
|
+
The installation with uv installs this package in an isolated environment and makes `glcli` available to run from any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
uv tool update-shell
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
To run the tests;
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
uv run pytest
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
# References
|
|
190
|
+
|
|
191
|
+
- [GreenLake Activate API Documentation](https://support.hpe.com/hpesc/public/docDisplay?docId=a00120791en_us&page=GUID-264278DA-9B2B-4E1E-9DED-596562E2CEF4.html)
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# GreenLake Activate CLI
|
|
2
2
|
|
|
3
|
-
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface.
|
|
3
|
+
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface. This tool uses the available API calls that HPE GreenLake Activate exposes for manging devices
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Query Activate inventory by:
|
|
8
|
+
- device name
|
|
9
|
+
- mac address
|
|
10
|
+
- serial number
|
|
11
|
+
- folder name
|
|
12
|
+
- folder id
|
|
13
|
+
- Move devices between folders
|
|
14
|
+
- Generate CLI commands for allowlist entries (WIP)
|
|
6
15
|
|
|
7
16
|
## Requirements
|
|
8
17
|
|
|
9
|
-
- Python 3.
|
|
18
|
+
- Python 3.12 or newer
|
|
10
19
|
- An HPE GreenLake Activate API credential
|
|
11
20
|
- Network access to `activate.arubanetworks.com`
|
|
12
21
|
|
|
13
|
-
The CLI uses the following Python packages, which are installed automatically:
|
|
14
|
-
|
|
15
|
-
- Typer
|
|
16
|
-
- Requests
|
|
17
|
-
- Rich
|
|
18
|
-
- Python Dotenv
|
|
19
|
-
|
|
20
22
|
## Installation
|
|
21
23
|
|
|
22
24
|
I recommend installing this package with `uv`. [Install uv](https://docs.astral.sh/uv/getting-started/installation/) if it is not already available.
|
|
@@ -46,13 +48,6 @@ Verify the installation:
|
|
|
46
48
|
glcli --version
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
This installs the CLI in an isolated environment and makes `glcli` available from
|
|
50
|
-
any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
51
|
-
|
|
52
|
-
```sh
|
|
53
|
-
uv tool update-shell
|
|
54
|
-
```
|
|
55
|
-
|
|
56
51
|
To upgrade or remove the installed CLI:
|
|
57
52
|
|
|
58
53
|
```sh
|
|
@@ -62,23 +57,22 @@ uv tool uninstall greenlake-activate-cli
|
|
|
62
57
|
|
|
63
58
|
### Install With Python and pip
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
repository:
|
|
60
|
+
Creating a virtual environment and installing the package:
|
|
67
61
|
|
|
68
62
|
```sh
|
|
69
|
-
python3
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
python3 -m venv .venv
|
|
64
|
+
source .venv/bin/activate
|
|
65
|
+
python3 -m pip install --upgrade pip
|
|
66
|
+
python3 -m pip install greenlake-activate-cli
|
|
73
67
|
```
|
|
74
68
|
|
|
75
|
-
The `glcli` command is then available
|
|
69
|
+
The `glcli` command is then available from the venv:
|
|
76
70
|
|
|
77
71
|
```sh
|
|
78
72
|
glcli --version
|
|
79
73
|
```
|
|
80
74
|
|
|
81
|
-
##
|
|
75
|
+
## Configuration
|
|
82
76
|
|
|
83
77
|
Run the CLI `glcli configure` command.
|
|
84
78
|
|
|
@@ -86,7 +80,7 @@ Run the CLI `glcli configure` command.
|
|
|
86
80
|
glcli configure
|
|
87
81
|
```
|
|
88
82
|
|
|
89
|
-
You will be prompted to enter your GreenLake Activate API token. This text is hidden.
|
|
83
|
+
You will be prompted to enter (paste) in your GreenLake Activate API token. This text is hidden.
|
|
90
84
|
|
|
91
85
|
This command is essentially doing this:
|
|
92
86
|
|
|
@@ -137,6 +131,20 @@ glcli query folder 5297450
|
|
|
137
131
|
glcli query folder SiteA-South
|
|
138
132
|
```
|
|
139
133
|
|
|
134
|
+
Move a device by serial number or MAC address:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
glcli move PHWLKAS02 SiteA-South
|
|
138
|
+
glcli move aa:bb:cc:00:11:22 5297450
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Move multiple devices from a file:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
glcli move serials serials.csv SiteA-South
|
|
145
|
+
glcli move macs mac_addresses.csv 5297450
|
|
146
|
+
```
|
|
147
|
+
|
|
140
148
|
Serial numbers and MAC addresses can also be loaded from CSV or newline-delimited files:
|
|
141
149
|
|
|
142
150
|
```sh
|
|
@@ -144,9 +152,6 @@ glcli query serial --file serials.csv
|
|
|
144
152
|
glcli query mac --file mac_addresses.csv
|
|
145
153
|
```
|
|
146
154
|
|
|
147
|
-
The file reader uses the first column. Serial files may use a `serial`, `serialNumber`,
|
|
148
|
-
`serial_number`, `serial number`, or `sn` header.
|
|
149
|
-
|
|
150
155
|
Use `--verbose` for diagnostic logging:
|
|
151
156
|
|
|
152
157
|
```sh
|
|
@@ -155,3 +160,21 @@ glcli --verbose query serial PHWLKAS02
|
|
|
155
160
|
|
|
156
161
|
Exit codes are `0` for a complete result, `1` when no devices are returned, and
|
|
157
162
|
`2` when at least one requested identifier is missing.
|
|
163
|
+
|
|
164
|
+
## Troubleshooting
|
|
165
|
+
|
|
166
|
+
The installation with uv installs this package in an isolated environment and makes `glcli` available to run from any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
uv tool update-shell
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
To run the tests;
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
uv run pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
# References
|
|
179
|
+
|
|
180
|
+
- [GreenLake Activate API Documentation](https://support.hpe.com/hpesc/public/docDisplay?docId=a00120791en_us&page=GUID-264278DA-9B2B-4E1E-9DED-596562E2CEF4.html)
|
|
@@ -7,6 +7,7 @@ from glcli.activate_login import create_activate_session, load_credentials, save
|
|
|
7
7
|
from glcli.data_parsing import parse_inventory_response
|
|
8
8
|
from glcli.query_inventory import query_inventory, read_identifiers_from_file
|
|
9
9
|
from glcli.query_folder import resolve_folder_ids
|
|
10
|
+
from glcli.move_device import move_device, resolve_move_macs, MAC_PATTERN
|
|
10
11
|
from glcli.display_data import display_inventory_sn
|
|
11
12
|
from rich.logging import RichHandler
|
|
12
13
|
|
|
@@ -98,6 +99,33 @@ def _query_folder(values: list[str]) -> None:
|
|
|
98
99
|
raise typer.Exit(code=1)
|
|
99
100
|
|
|
100
101
|
|
|
102
|
+
def _move(identifier_type: str, identifiers: list[str], destination: str) -> None:
|
|
103
|
+
identifiers = [identifier.strip().upper() for identifier in identifiers if identifier.strip()]
|
|
104
|
+
destination = destination.strip()
|
|
105
|
+
if not identifiers:
|
|
106
|
+
raise typer.BadParameter("At least one identifier is required")
|
|
107
|
+
if not destination:
|
|
108
|
+
raise typer.BadParameter("A destination folder is required")
|
|
109
|
+
|
|
110
|
+
identifiers = list(dict.fromkeys(identifiers))
|
|
111
|
+
credential = _load_credential()
|
|
112
|
+
session = None
|
|
113
|
+
try:
|
|
114
|
+
session = create_activate_session(credential)
|
|
115
|
+
mac_addresses, unresolved = resolve_move_macs(session, identifier_type, identifiers)
|
|
116
|
+
if unresolved:
|
|
117
|
+
typer.echo(f"Not found or missing MAC: {', '.join(unresolved)}", err=True)
|
|
118
|
+
if not mac_addresses:
|
|
119
|
+
raise typer.Exit(code=1)
|
|
120
|
+
move_device(session, mac_addresses, destination)
|
|
121
|
+
finally:
|
|
122
|
+
if session is not None:
|
|
123
|
+
session.close()
|
|
124
|
+
|
|
125
|
+
if unresolved:
|
|
126
|
+
raise typer.Exit(code=2)
|
|
127
|
+
|
|
128
|
+
|
|
101
129
|
@app.callback()
|
|
102
130
|
def main_callback(
|
|
103
131
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debugging output to console."),
|
|
@@ -117,6 +145,41 @@ def configure() -> None:
|
|
|
117
145
|
typer.echo(f"Credential saved to {config_path}")
|
|
118
146
|
|
|
119
147
|
|
|
148
|
+
@app.command("move")
|
|
149
|
+
def move(
|
|
150
|
+
arguments: list[str] = typer.Argument(..., metavar="MOVE_ARGUMENT"),
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Move a device or a file of devices to an Activate folder.
|
|
153
|
+
|
|
154
|
+
Forms:
|
|
155
|
+
move IDENTIFIER DESTINATION
|
|
156
|
+
move serials FILE DESTINATION
|
|
157
|
+
move macs FILE DESTINATION
|
|
158
|
+
|
|
159
|
+
examples:
|
|
160
|
+
move SERIALNO12 Site_A
|
|
161
|
+
move AA:BB:CC:DD:EE:FF Site_B
|
|
162
|
+
move serials ./serials.csv Site_C
|
|
163
|
+
move macs ./macs.csv Site_D
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
if arguments[0] in {"serials", "macs"}:
|
|
167
|
+
if len(arguments) != 3:
|
|
168
|
+
raise typer.BadParameter("Expected: move serials|macs FILENAME DESTINATION")
|
|
169
|
+
identifier_type = "serial" if arguments[0] == "serials" else "mac"
|
|
170
|
+
try:
|
|
171
|
+
identifiers = read_identifiers_from_file(Path(arguments[1]), identifier_type)
|
|
172
|
+
except (OSError, ValueError) as exc:
|
|
173
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
174
|
+
_move(identifier_type, identifiers, arguments[2])
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if len(arguments) != 2:
|
|
178
|
+
raise typer.BadParameter("Expected: move IDENTIFIER DESTINATION")
|
|
179
|
+
identifier_type = "mac" if MAC_PATTERN.fullmatch(arguments[0].strip()) else "serial"
|
|
180
|
+
_move(identifier_type, [arguments[0]], arguments[1])
|
|
181
|
+
|
|
182
|
+
|
|
120
183
|
@query_app.command("serial")
|
|
121
184
|
def query_serial(
|
|
122
185
|
serials: list[str] | None = typer.Argument(None, metavar="SERIAL"),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from glcli.query_inventory import query_inventory
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
UPDATE_INVENTORY_URL = "https://activate.arubanetworks.com/api/ext/inventory.json?action=update"
|
|
10
|
+
REQUEST_TIMEOUT = 30
|
|
11
|
+
MAC_PATTERN = re.compile(r"^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$", re.IGNORECASE)
|
|
12
|
+
|
|
13
|
+
def move_device(
|
|
14
|
+
session: requests.Session,
|
|
15
|
+
mac_addresses: list[str],
|
|
16
|
+
destination_folder: str,
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Move MAC addresses to a folder name or numeric folder ID."""
|
|
19
|
+
if not mac_addresses:
|
|
20
|
+
raise ValueError("At least one MAC address is required")
|
|
21
|
+
destination_folder = destination_folder.strip()
|
|
22
|
+
if not destination_folder:
|
|
23
|
+
raise ValueError("A destination folder is required")
|
|
24
|
+
|
|
25
|
+
destination_key = "folderId" if destination_folder.isdigit() else "folderName"
|
|
26
|
+
payload = {
|
|
27
|
+
"devices": [
|
|
28
|
+
{"mac": mac, destination_key: destination_folder}
|
|
29
|
+
for mac in mac_addresses
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
raw_data = f"json={json.dumps(payload)}"
|
|
33
|
+
logger.debug("Move request: %s", raw_data)
|
|
34
|
+
response = session.post(UPDATE_INVENTORY_URL, data=raw_data, timeout=REQUEST_TIMEOUT)
|
|
35
|
+
if response.status_code != 200:
|
|
36
|
+
logger.error("Move failed with status code: %s", response.status_code)
|
|
37
|
+
raise RuntimeError("Inventory move failed")
|
|
38
|
+
|
|
39
|
+
logger.info("Moved %d device(s) to %s", len(mac_addresses), destination_folder)
|
|
40
|
+
return response.text
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_move_macs(
|
|
44
|
+
session: requests.Session,
|
|
45
|
+
identifier_type: str,
|
|
46
|
+
identifiers: list[str],
|
|
47
|
+
) -> tuple[list[str], list[str]]:
|
|
48
|
+
"""Resolve valid serial or MAC identifiers to canonical MAC addresses."""
|
|
49
|
+
if identifier_type not in {"serial", "mac"}:
|
|
50
|
+
raise ValueError(f"Unsupported move identifier type: {identifier_type}")
|
|
51
|
+
|
|
52
|
+
response_text, missing = query_inventory(session, identifier_type, identifiers)
|
|
53
|
+
try:
|
|
54
|
+
response = json.loads(response_text)
|
|
55
|
+
except json.JSONDecodeError as exc:
|
|
56
|
+
raise RuntimeError("Activate returned invalid inventory JSON") from exc
|
|
57
|
+
|
|
58
|
+
resolved: list[str] = []
|
|
59
|
+
unresolved = list(missing)
|
|
60
|
+
for device in response.get("devices", []):
|
|
61
|
+
if not isinstance(device, dict):
|
|
62
|
+
continue
|
|
63
|
+
mac = str(device.get("mac") or "").strip().upper()
|
|
64
|
+
if not MAC_PATTERN.fullmatch(mac):
|
|
65
|
+
identifier = device.get("serialNumber") if identifier_type == "serial" else device.get("mac")
|
|
66
|
+
if identifier:
|
|
67
|
+
unresolved.append(str(identifier))
|
|
68
|
+
continue
|
|
69
|
+
resolved.append(mac)
|
|
70
|
+
|
|
71
|
+
return list(dict.fromkeys(resolved)), list(dict.fromkeys(unresolved))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenlake-activate-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A CLI tool used to interact with HPE GreenLake Activate
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -11,23 +11,25 @@ Requires-Dist: typer>=0.27.2
|
|
|
11
11
|
|
|
12
12
|
# GreenLake Activate CLI
|
|
13
13
|
|
|
14
|
-
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface.
|
|
14
|
+
The purpose of this is to help manage devices in HPE GreenLake Activate with an easy to use CLI interface. This tool uses the available API calls that HPE GreenLake Activate exposes for manging devices
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Query Activate inventory by:
|
|
19
|
+
- device name
|
|
20
|
+
- mac address
|
|
21
|
+
- serial number
|
|
22
|
+
- folder name
|
|
23
|
+
- folder id
|
|
24
|
+
- Move devices between folders
|
|
25
|
+
- Generate CLI commands for allowlist entries (WIP)
|
|
17
26
|
|
|
18
27
|
## Requirements
|
|
19
28
|
|
|
20
|
-
- Python 3.
|
|
29
|
+
- Python 3.12 or newer
|
|
21
30
|
- An HPE GreenLake Activate API credential
|
|
22
31
|
- Network access to `activate.arubanetworks.com`
|
|
23
32
|
|
|
24
|
-
The CLI uses the following Python packages, which are installed automatically:
|
|
25
|
-
|
|
26
|
-
- Typer
|
|
27
|
-
- Requests
|
|
28
|
-
- Rich
|
|
29
|
-
- Python Dotenv
|
|
30
|
-
|
|
31
33
|
## Installation
|
|
32
34
|
|
|
33
35
|
I recommend installing this package with `uv`. [Install uv](https://docs.astral.sh/uv/getting-started/installation/) if it is not already available.
|
|
@@ -57,13 +59,6 @@ Verify the installation:
|
|
|
57
59
|
glcli --version
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
This installs the CLI in an isolated environment and makes `glcli` available from
|
|
61
|
-
any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
62
|
-
|
|
63
|
-
```sh
|
|
64
|
-
uv tool update-shell
|
|
65
|
-
```
|
|
66
|
-
|
|
67
62
|
To upgrade or remove the installed CLI:
|
|
68
63
|
|
|
69
64
|
```sh
|
|
@@ -73,23 +68,22 @@ uv tool uninstall greenlake-activate-cli
|
|
|
73
68
|
|
|
74
69
|
### Install With Python and pip
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
repository:
|
|
71
|
+
Creating a virtual environment and installing the package:
|
|
78
72
|
|
|
79
73
|
```sh
|
|
80
|
-
python3
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
python3 -m venv .venv
|
|
75
|
+
source .venv/bin/activate
|
|
76
|
+
python3 -m pip install --upgrade pip
|
|
77
|
+
python3 -m pip install greenlake-activate-cli
|
|
84
78
|
```
|
|
85
79
|
|
|
86
|
-
The `glcli` command is then available
|
|
80
|
+
The `glcli` command is then available from the venv:
|
|
87
81
|
|
|
88
82
|
```sh
|
|
89
83
|
glcli --version
|
|
90
84
|
```
|
|
91
85
|
|
|
92
|
-
##
|
|
86
|
+
## Configuration
|
|
93
87
|
|
|
94
88
|
Run the CLI `glcli configure` command.
|
|
95
89
|
|
|
@@ -97,7 +91,7 @@ Run the CLI `glcli configure` command.
|
|
|
97
91
|
glcli configure
|
|
98
92
|
```
|
|
99
93
|
|
|
100
|
-
You will be prompted to enter your GreenLake Activate API token. This text is hidden.
|
|
94
|
+
You will be prompted to enter (paste) in your GreenLake Activate API token. This text is hidden.
|
|
101
95
|
|
|
102
96
|
This command is essentially doing this:
|
|
103
97
|
|
|
@@ -148,6 +142,20 @@ glcli query folder 5297450
|
|
|
148
142
|
glcli query folder SiteA-South
|
|
149
143
|
```
|
|
150
144
|
|
|
145
|
+
Move a device by serial number or MAC address:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
glcli move PHWLKAS02 SiteA-South
|
|
149
|
+
glcli move aa:bb:cc:00:11:22 5297450
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Move multiple devices from a file:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
glcli move serials serials.csv SiteA-South
|
|
156
|
+
glcli move macs mac_addresses.csv 5297450
|
|
157
|
+
```
|
|
158
|
+
|
|
151
159
|
Serial numbers and MAC addresses can also be loaded from CSV or newline-delimited files:
|
|
152
160
|
|
|
153
161
|
```sh
|
|
@@ -155,9 +163,6 @@ glcli query serial --file serials.csv
|
|
|
155
163
|
glcli query mac --file mac_addresses.csv
|
|
156
164
|
```
|
|
157
165
|
|
|
158
|
-
The file reader uses the first column. Serial files may use a `serial`, `serialNumber`,
|
|
159
|
-
`serial_number`, `serial number`, or `sn` header.
|
|
160
|
-
|
|
161
166
|
Use `--verbose` for diagnostic logging:
|
|
162
167
|
|
|
163
168
|
```sh
|
|
@@ -166,3 +171,21 @@ glcli --verbose query serial PHWLKAS02
|
|
|
166
171
|
|
|
167
172
|
Exit codes are `0` for a complete result, `1` when no devices are returned, and
|
|
168
173
|
`2` when at least one requested identifier is missing.
|
|
174
|
+
|
|
175
|
+
## Troubleshooting
|
|
176
|
+
|
|
177
|
+
The installation with uv installs this package in an isolated environment and makes `glcli` available to run from any directory. If uv reports that its tool directory is not on your `PATH`, run:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
uv tool update-shell
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
To run the tests;
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
uv run pytest
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
# References
|
|
190
|
+
|
|
191
|
+
- [GreenLake Activate API Documentation](https://support.hpe.com/hpesc/public/docDisplay?docId=a00120791en_us&page=GUID-264278DA-9B2B-4E1E-9DED-596562E2CEF4.html)
|
|
@@ -5,6 +5,7 @@ src/glcli/activate_login.py
|
|
|
5
5
|
src/glcli/cli.py
|
|
6
6
|
src/glcli/data_parsing.py
|
|
7
7
|
src/glcli/display_data.py
|
|
8
|
+
src/glcli/move_device.py
|
|
8
9
|
src/glcli/query_folder.py
|
|
9
10
|
src/glcli/query_inventory.py
|
|
10
11
|
src/greenlake_activate_cli.egg-info/PKG-INFO
|
|
@@ -17,5 +18,7 @@ tests/test_activate_login.py
|
|
|
17
18
|
tests/test_cli.py
|
|
18
19
|
tests/test_configure.py
|
|
19
20
|
tests/test_data_parsing.py
|
|
21
|
+
tests/test_move_cli.py
|
|
22
|
+
tests/test_move_device.py
|
|
20
23
|
tests/test_query_folder.py
|
|
21
24
|
tests/test_query_inventory.py
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typer.testing import CliRunner
|
|
2
|
+
|
|
3
|
+
from glcli import cli
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeSession:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.closed = False
|
|
9
|
+
|
|
10
|
+
def close(self):
|
|
11
|
+
self.closed = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_direct_serial_move_resolves_and_updates(monkeypatch):
|
|
15
|
+
runner = CliRunner()
|
|
16
|
+
session = FakeSession()
|
|
17
|
+
captured = {}
|
|
18
|
+
|
|
19
|
+
monkeypatch.setattr(cli, "load_credentials", lambda: "token")
|
|
20
|
+
monkeypatch.setattr(cli, "create_activate_session", lambda credential: session)
|
|
21
|
+
monkeypatch.setattr(
|
|
22
|
+
cli,
|
|
23
|
+
"resolve_move_macs",
|
|
24
|
+
lambda active_session, identifier_type, identifiers: (["AA:BB:CC:DD:EE:FF"], []),
|
|
25
|
+
)
|
|
26
|
+
monkeypatch.setattr(
|
|
27
|
+
cli,
|
|
28
|
+
"move_device",
|
|
29
|
+
lambda active_session, macs, destination: captured.update(
|
|
30
|
+
macs=macs, destination=destination
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
result = runner.invoke(cli.app, ["move", "SN1", "Northwest"])
|
|
35
|
+
|
|
36
|
+
assert result.exit_code == 0
|
|
37
|
+
assert captured == {"macs": ["AA:BB:CC:DD:EE:FF"], "destination": "Northwest"}
|
|
38
|
+
assert session.closed is True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_mac_file_move_uses_mac_identifier_type(monkeypatch, tmp_path):
|
|
42
|
+
runner = CliRunner()
|
|
43
|
+
captured = {}
|
|
44
|
+
mac_file = tmp_path / "macs.txt"
|
|
45
|
+
mac_file.write_text("aa:bb:cc:dd:ee:ff\n")
|
|
46
|
+
|
|
47
|
+
monkeypatch.setattr(cli, "load_credentials", lambda: "token")
|
|
48
|
+
monkeypatch.setattr(cli, "create_activate_session", lambda credential: FakeSession())
|
|
49
|
+
monkeypatch.setattr(cli, "read_identifiers_from_file", lambda path, kind: ["AA:BB:CC:DD:EE:FF"])
|
|
50
|
+
monkeypatch.setattr(
|
|
51
|
+
cli,
|
|
52
|
+
"resolve_move_macs",
|
|
53
|
+
lambda active_session, identifier_type, identifiers: (["AA:BB:CC:DD:EE:FF"], []),
|
|
54
|
+
)
|
|
55
|
+
monkeypatch.setattr(
|
|
56
|
+
cli,
|
|
57
|
+
"move_device",
|
|
58
|
+
lambda active_session, macs, destination: captured.update(
|
|
59
|
+
macs=macs, destination=destination
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
result = runner.invoke(cli.app, ["move", "macs", str(mac_file), "5297450"])
|
|
64
|
+
|
|
65
|
+
assert result.exit_code == 0
|
|
66
|
+
assert captured["macs"] == ["AA:BB:CC:DD:EE:FF"]
|
|
67
|
+
assert captured["destination"] == "5297450"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_move_continues_with_valid_devices(monkeypatch):
|
|
71
|
+
runner = CliRunner()
|
|
72
|
+
captured = {}
|
|
73
|
+
|
|
74
|
+
monkeypatch.setattr(cli, "load_credentials", lambda: "token")
|
|
75
|
+
monkeypatch.setattr(cli, "create_activate_session", lambda credential: FakeSession())
|
|
76
|
+
monkeypatch.setattr(
|
|
77
|
+
cli,
|
|
78
|
+
"resolve_move_macs",
|
|
79
|
+
lambda active_session, identifier_type, identifiers: (["AA:BB:CC:DD:EE:FF"], ["SN2"]),
|
|
80
|
+
)
|
|
81
|
+
monkeypatch.setattr(
|
|
82
|
+
cli,
|
|
83
|
+
"move_device",
|
|
84
|
+
lambda active_session, macs, destination: captured.update(macs=macs),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
result = runner.invoke(cli.app, ["move", "SN1", "Northwest"])
|
|
88
|
+
|
|
89
|
+
assert result.exit_code == 2
|
|
90
|
+
assert captured["macs"] == ["AA:BB:CC:DD:EE:FF"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from glcli.move_device import move_device, resolve_move_macs
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeResponse:
|
|
9
|
+
def __init__(self, body, status_code=200):
|
|
10
|
+
self.text = json.dumps(body) if not isinstance(body, str) else body
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeSession:
|
|
15
|
+
def __init__(self, responses):
|
|
16
|
+
self.responses = iter(responses)
|
|
17
|
+
self.calls = []
|
|
18
|
+
|
|
19
|
+
def post(self, url, data, timeout):
|
|
20
|
+
self.calls.append((url, data, timeout))
|
|
21
|
+
return next(self.responses)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_move_uses_folder_name_payload_and_session():
|
|
25
|
+
session = FakeSession([FakeResponse({"message": {"code": 0}})])
|
|
26
|
+
|
|
27
|
+
move_device(session, ["AA:BB:CC:DD:EE:FF"], "Northwest")
|
|
28
|
+
|
|
29
|
+
assert json.loads(session.calls[0][1].removeprefix("json=")) == {
|
|
30
|
+
"devices": [{"mac": "AA:BB:CC:DD:EE:FF", "folderName": "Northwest"}]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_move_uses_folder_id_payload_for_numeric_destination():
|
|
35
|
+
session = FakeSession([FakeResponse({"message": {"code": 0}})])
|
|
36
|
+
|
|
37
|
+
move_device(session, ["AA:BB:CC:DD:EE:FF", "11:22:33:44:55:66"], "5297450")
|
|
38
|
+
|
|
39
|
+
assert json.loads(session.calls[0][1].removeprefix("json=")) == {
|
|
40
|
+
"devices": [
|
|
41
|
+
{"mac": "AA:BB:CC:DD:EE:FF", "folderId": "5297450"},
|
|
42
|
+
{"mac": "11:22:33:44:55:66", "folderId": "5297450"},
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_resolve_serials_to_macs():
|
|
48
|
+
session = FakeSession([
|
|
49
|
+
FakeResponse({"devices": [{"serialNumber": "SN1", "mac": "AA:BB:CC:DD:EE:FF"}]}),
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
macs, missing = resolve_move_macs(session, "serial", ["SN1"])
|
|
53
|
+
|
|
54
|
+
assert macs == ["AA:BB:CC:DD:EE:FF"]
|
|
55
|
+
assert missing == []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_move_rejects_failed_update():
|
|
59
|
+
session = FakeSession([FakeResponse({"error": "failed"}, status_code=500)])
|
|
60
|
+
|
|
61
|
+
with pytest.raises(RuntimeError, match="move failed"):
|
|
62
|
+
move_device(session, ["AA:BB:CC:DD:EE:FF"], "Northwest")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|