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.
Files changed (26) hide show
  1. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/PKG-INFO +53 -30
  2. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/README.md +52 -29
  3. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/pyproject.toml +1 -1
  4. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/cli.py +63 -0
  5. greenlake_activate_cli-0.3.0/src/glcli/move_device.py +71 -0
  6. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/PKG-INFO +53 -30
  7. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/SOURCES.txt +3 -0
  8. greenlake_activate_cli-0.3.0/tests/test_move_cli.py +90 -0
  9. greenlake_activate_cli-0.3.0/tests/test_move_device.py +62 -0
  10. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/setup.cfg +0 -0
  11. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/__init__.py +0 -0
  12. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/activate_login.py +0 -0
  13. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/data_parsing.py +0 -0
  14. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/display_data.py +0 -0
  15. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/query_folder.py +0 -0
  16. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/glcli/query_inventory.py +0 -0
  17. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/dependency_links.txt +0 -0
  18. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/entry_points.txt +0 -0
  19. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/requires.txt +0 -0
  20. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/src/greenlake_activate_cli.egg-info/top_level.txt +0 -0
  21. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_activate_login.py +0 -0
  22. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_cli.py +0 -0
  23. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_configure.py +0 -0
  24. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_data_parsing.py +0 -0
  25. {greenlake_activate_cli-0.2.2 → greenlake_activate_cli-0.3.0}/tests/test_query_folder.py +0 -0
  26. {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.2.2
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
- # Setup
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.14 or newer
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
- Create a virtual environment, activate it, and install the project from the cloned
77
- repository:
71
+ Creating a virtual environment and installing the package:
78
72
 
79
73
  ```sh
80
- python3.14 -m venv .venv
81
- . .venv/bin/activate
82
- python -m pip install --upgrade pip
83
- python -m pip install .
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 directly:
80
+ The `glcli` command is then available from the venv:
87
81
 
88
82
  ```sh
89
83
  glcli --version
90
84
  ```
91
85
 
92
- ## Configure Credentials
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
- # Setup
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.14 or newer
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
- Create a virtual environment, activate it, and install the project from the cloned
66
- repository:
60
+ Creating a virtual environment and installing the package:
67
61
 
68
62
  ```sh
69
- python3.14 -m venv .venv
70
- . .venv/bin/activate
71
- python -m pip install --upgrade pip
72
- python -m pip install .
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 directly:
69
+ The `glcli` command is then available from the venv:
76
70
 
77
71
  ```sh
78
72
  glcli --version
79
73
  ```
80
74
 
81
- ## Configure Credentials
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "greenlake-activate-cli"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "A CLI tool used to interact with HPE GreenLake Activate"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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.2.2
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
- # Setup
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.14 or newer
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
- Create a virtual environment, activate it, and install the project from the cloned
77
- repository:
71
+ Creating a virtual environment and installing the package:
78
72
 
79
73
  ```sh
80
- python3.14 -m venv .venv
81
- . .venv/bin/activate
82
- python -m pip install --upgrade pip
83
- python -m pip install .
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 directly:
80
+ The `glcli` command is then available from the venv:
87
81
 
88
82
  ```sh
89
83
  glcli --version
90
84
  ```
91
85
 
92
- ## Configure Credentials
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")