Skip to content

Commit 097ea2c

Browse files
authored
Misc devices command bugfixes (#204)
* fix nightly test runs * remove dupe command * stop testing python3.5 * revert * in `devices show`, don't echo "backupUsage" again when --format is JSON/RAW, since it's already contained in the first JSON output * drop CSV --format option from `devices show` since it doesn't really make sense given the data * clarify --days-since-last-connected help text a bit * add --inactive option to make it possible to select only deactivated devices * add new date option placeholders * refactor magic date parsing into a click type, and make search options more flexible * remove redundant import * use MagicDate in date options * fix option ordering * rename options * make magic time period case-insensitive * allow case-insensitive periods in regex * inadvertent commit * set UTC timezone explicitly in MagicDate result * use correct method to set UTC (not convert to UTC) * add negative tests for MagicDate, adjust timestamp regex * use .timestamp() for converting from dt > timestamp * add note to changelog * add connection/creation date filter logic, remove 'drop_most_recent' option and logic, clean up some help strings, update tests * fix JSON/RAW-JSON dataframe output format bug * fix recursion error trying to convert backup set data to json * style and dupe changelog entry * fix output format tests * remove unused imports * "the" provided value * improve error handling in _deactivate_device() * fix read_csv() * style * fix 3.8+ error about iteration over dict while modifying it * add docs and fix doc error with trailing underscore * put back --drop-most-recent as --exclude-most-recently-connected * add list-backup-sets to changelog * typo * standardize triple-quoted strings to single-quoted * wrap long help strings * simplify read_csv a bit * add more read_csv tests * style * add test for invalid guid check * use elif
1 parent ed35f61 commit 097ea2c

9 files changed

Lines changed: 284 additions & 161 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1616
- `devices deactivate` to deactivate a single computer.
1717
- `devices show` to retrieve detailed information about a computer.
1818
- `devices list` to retrieve info about many devices, including device settings.
19+
- `devices list-backup-sets` to retrieve detailed info about device backup sets.
1920
- `devices bulk deactivate` to deactivate a list of devices.
2021

2122
- `code42 departing-employee list` command.
@@ -32,7 +33,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
3233
- `code42 cases file-events` commands:
3334
- `add` to add an event to a case.
3435
- `remove` to remove an event from the case.
35-
- `list` to view all events assocaited to the case.
36+
- `list` to view all events associated to the case.
3637

3738
### Changed
3839

docs/commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* [Alerts](commands/alerts.rst)
77
* [Alert Rules](commands/alertrules.rst)
88
* [Departing Employee](commands/departingemployee.rst)
9+
* [Devices](commands/devices.rst)
910
* [High Risk Employee](commands/highriskemployee.rst)
1011
* [Legal Hold](commands/legalhold.rst)
1112
* [Cases](commands/cases.rst)

docs/commands/devices.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.. click:: code42cli.cmds.devices:devices
2+
:prog: devices
3+
:show-nested:

src/code42cli/cmds/devices.py

Lines changed: 106 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from datetime import date
2-
from datetime import datetime
32

43
import click
54
from pandas import concat
65
from pandas import DataFrame
76
from pandas import to_datetime
8-
from pandas import to_timedelta
97
from py42 import exceptions
108
from py42.exceptions import Py42NotFoundError
119

1210
from code42cli.bulk import run_bulk_process
1311
from code42cli.click_ext.groups import OrderedGroup
12+
from code42cli.click_ext.options import incompatible_with
13+
from code42cli.click_ext.types import MagicDate
14+
from code42cli.date_helper import round_datetime_to_day_end
15+
from code42cli.date_helper import round_datetime_to_day_start
1416
from code42cli.errors import Code42CLIError
1517
from code42cli.file_readers import read_csv_arg
1618
from code42cli.options import format_option
@@ -33,8 +35,7 @@ def devices(state):
3335
required=False,
3436
is_flag=True,
3537
default=False,
36-
help="""Prepend "deactivated_" and today's date to the name of any
37-
deactivated devices.""",
38+
help="Prepend 'deactivated_<current_date>' to the name of any deactivated devices.",
3839
)
3940

4041
DATE_FORMAT = "%Y-%m-%d"
@@ -43,8 +44,8 @@ def devices(state):
4344
required=False,
4445
type=click.DateTime(formats=[DATE_FORMAT]),
4546
default=None,
46-
help="""The date on which the archive should be purged from cold storage in yyyy-MM-dd format.
47-
If not provided, the date will be set according to the appropriate org settings.""",
47+
help="The date on which the archive should be purged from cold storage in yyyy-MM-dd format. "
48+
"If not provided, the date will be set according to the appropriate org settings.",
4849
)
4950

5051

@@ -59,8 +60,12 @@ def deactivate(state, device_guid, change_device_name, purge_date):
5960

6061

6162
def _deactivate_device(sdk, device_guid, change_device_name, purge_date):
62-
device = sdk.devices.get_by_guid(device_guid)
6363
try:
64+
int(device_guid)
65+
except ValueError:
66+
raise Code42CLIError("Not a valid guid.")
67+
try:
68+
device = sdk.devices.get_by_guid(device_guid)
6469
sdk.devices.deactivate(device.data["computerId"])
6570
except exceptions.Py42BadRequestError:
6671
raise Code42CLIError("The device {} is in legal hold.".format(device_guid))
@@ -103,10 +108,9 @@ def _change_device_name(sdk, guid, name):
103108

104109
@devices.command()
105110
@device_guid_argument
106-
@format_option
107111
@sdk_options()
108112
def show(state, device_guid, format=None):
109-
"""Print device info. Requires device GUID."""
113+
"""Print individual device info. Requires device GUID."""
110114

111115
formatter = OutputFormatter(format, _device_info_keys_map())
112116
backup_set_formatter = OutputFormatter(format, _backup_set_keys_map())
@@ -145,20 +149,23 @@ def _get_device_info(sdk, device_guid):
145149

146150
active_option = click.option(
147151
"--active",
148-
required=False,
149-
type=bool,
150152
is_flag=True,
153+
help="Limits results to only active devices.",
151154
default=None,
152-
help="Get only active or deactivated devices. Defaults to getting all devices.",
153155
)
154-
156+
inactive_option = click.option(
157+
"--inactive",
158+
is_flag=True,
159+
help="Limits results to only deactivated devices.",
160+
cls=incompatible_with("active"),
161+
)
155162
org_uid_option = click.option(
156163
"--org-uid",
157164
required=False,
158165
type=str,
159166
default=None,
160-
help="""Limit devices to only the ones in the org you specify.
161-
Note that child orgs will be included.""",
167+
help="Limit devices to only the ones in the org you specify. "
168+
"Note that child orgs will be included.",
162169
)
163170

164171
include_usernames_option = click.option(
@@ -171,31 +178,18 @@ def _get_device_info(sdk, device_guid):
171178
)
172179

173180

174-
@devices.command(name="list", help="Get information about many devices")
181+
@devices.command(name="list")
175182
@active_option
176-
@click.option(
177-
"--days-since-last-connected",
178-
required=False,
179-
type=int,
180-
help="Return only devices that have not connected in the number of days specified.",
181-
)
183+
@inactive_option
182184
@org_uid_option
183-
@click.option(
184-
"--drop-most-recent",
185-
required=False,
186-
type=int,
187-
help="""Will drop the X most recently connected devices for each user from the
188-
result list where X is the number you provide as this argument. Can be used to
189-
avoid passing the most recently connected device for a user to the deactivate command.""",
190-
)
191185
@click.option(
192186
"--include-backup-usage",
193187
required=False,
194188
type=bool,
195189
default=False,
196190
is_flag=True,
197-
help="""Return backup usage information for each device
198-
(may significantly lengthen the size of the return).""",
191+
help="Return backup usage information for each device (may significantly lengthen the size "
192+
"of the return).",
199193
)
200194
@include_usernames_option
201195
@click.option(
@@ -204,53 +198,99 @@ def _get_device_info(sdk, device_guid):
204198
type=bool,
205199
default=False,
206200
is_flag=True,
207-
help="""Include device settings in output.""",
201+
help="Include device settings in output.",
202+
)
203+
@click.option(
204+
"--exclude-most-recently-connected",
205+
type=int,
206+
help="Filter out the N most recently connected devices per user. "
207+
"Useful for identifying duplicate and/or replaced devices that are no longer needed across "
208+
"an environment. If a user has 2 devices and N=1, the one device with the most recent "
209+
"'lastConnected' date will not show up in the result list.",
210+
)
211+
@click.option(
212+
"--last-connected-before",
213+
type=MagicDate(rounding_func=round_datetime_to_day_start),
214+
help=f"Include devices only when the 'lastConnected' field is after the provided value. {MagicDate.HELP_TEXT}",
215+
)
216+
@click.option(
217+
"--last-connected-after",
218+
type=MagicDate(rounding_func=round_datetime_to_day_end),
219+
help="Include devices only when 'lastConnected' field is after the provided value. "
220+
"Argument format options are the same as --last-connected-before.",
221+
)
222+
@click.option(
223+
"--created-before",
224+
type=MagicDate(rounding_func=round_datetime_to_day_start),
225+
help="Include devices only when 'creationDate' field is less than the provided value. "
226+
"Argument format options are the same as --last-connected-before.",
227+
)
228+
@click.option(
229+
"--created-after",
230+
type=MagicDate(rounding_func=round_datetime_to_day_end),
231+
help="Include devices only when 'creationDate' field is greater than the provided value. "
232+
"Argument format options are the same as --last-connected-before.",
208233
)
209234
@format_option
210235
@sdk_options()
211236
def list_devices(
212237
state,
213238
active,
214-
days_since_last_connected,
215-
drop_most_recent,
239+
inactive,
216240
org_uid,
217241
include_backup_usage,
218242
include_usernames,
219243
include_settings,
244+
exclude_most_recently_connected,
245+
last_connected_after,
246+
last_connected_before,
247+
created_after,
248+
created_before,
220249
format,
221250
):
222-
"""Outputs a list of all devices."""
251+
"""Get information about many devices."""
252+
if inactive:
253+
active = False
223254
columns = [
224255
"computerId",
225256
"guid",
226257
"name",
227258
"osHostname",
228259
"status",
229260
"lastConnected",
261+
"creationDate",
230262
"productVersion",
231263
"osName",
232264
"osVersion",
233265
"userUid",
234266
]
235-
devices_dataframe = _get_device_dataframe(
267+
df = _get_device_dataframe(
236268
state.sdk, columns, active, org_uid, include_backup_usage
237269
)
238-
if drop_most_recent:
239-
devices_dataframe = _drop_n_devices_per_user(
240-
devices_dataframe, drop_most_recent
241-
)
242-
if days_since_last_connected:
243-
devices_dataframe = _drop_devices_which_have_not_connected_in_some_number_of_days(
244-
devices_dataframe, days_since_last_connected
270+
if last_connected_after:
271+
df = df.loc[to_datetime(df.lastConnected) > last_connected_after]
272+
if last_connected_before:
273+
df = df.loc[to_datetime(df.lastConnected) < last_connected_before]
274+
if created_after:
275+
df = df.loc[to_datetime(df.creationDate) > created_after]
276+
if created_before:
277+
df = df.loc[to_datetime(df.creationDate) < created_before]
278+
if exclude_most_recently_connected:
279+
most_recent = (
280+
df.sort_values(["userUid", "lastConnected"], ascending=False)
281+
.groupby("userUid")
282+
.head(exclude_most_recently_connected)
245283
)
284+
df = df.drop(most_recent.index)
246285
if include_settings:
247-
devices_dataframe = _add_settings_to_dataframe(state.sdk, devices_dataframe)
286+
df = _add_settings_to_dataframe(state.sdk, df)
248287
if include_usernames:
249-
devices_dataframe = _add_usernames_to_device_dataframe(
250-
state.sdk, devices_dataframe
251-
)
252-
formatter = DataFrameOutputFormatter(format)
253-
formatter.echo_formatted_dataframe(devices_dataframe)
288+
df = _add_usernames_to_device_dataframe(state.sdk, df)
289+
if df.empty:
290+
click.echo("No results found.")
291+
else:
292+
formatter = DataFrameOutputFormatter(format)
293+
formatter.echo_formatted_dataframe(df)
254294

255295

256296
def _get_device_dataframe(
@@ -297,35 +337,6 @@ def handle_row(guid):
297337
return device_dataframe
298338

299339

300-
def _drop_devices_which_have_not_connected_in_some_number_of_days(
301-
devices_dataframe, days_since_last_connected
302-
):
303-
utc_now = to_datetime(datetime.utcnow(), utc=True)
304-
devices_last_connected_dates = to_datetime(
305-
devices_dataframe["lastConnected"], utc=True
306-
)
307-
days_since_last_connected_delta = to_timedelta(
308-
days_since_last_connected, unit="days"
309-
)
310-
return devices_dataframe.loc[
311-
utc_now - devices_last_connected_dates > days_since_last_connected_delta, :,
312-
]
313-
314-
315-
def _drop_n_devices_per_user(
316-
device_dataframe,
317-
number_to_drop,
318-
sort_field="lastConnected",
319-
sort_ascending=False,
320-
group_field="userUid",
321-
):
322-
return (
323-
device_dataframe.sort_values(by=sort_field, ascending=sort_ascending)
324-
.drop(device_dataframe.groupby(group_field).head(number_to_drop).index)
325-
.reset_index(drop=True)
326-
)
327-
328-
329340
def _add_usernames_to_device_dataframe(sdk, device_dataframe):
330341
users_generator = sdk.users.get_all()
331342
users_list = []
@@ -337,30 +348,29 @@ def _add_usernames_to_device_dataframe(sdk, device_dataframe):
337348
return device_dataframe.merge(users_dataframe, how="left", on="userUid")
338349

339350

340-
@devices.command(
341-
name="list-backup-sets",
342-
help="Get information about many devices and their backup sets",
343-
)
351+
@devices.command()
344352
@active_option
353+
@inactive_option
345354
@org_uid_option
346355
@include_usernames_option
347356
@format_option
348357
@sdk_options()
349358
def list_backup_sets(
350-
state, active, org_uid, include_usernames, format,
359+
state, active, inactive, org_uid, include_usernames, format,
351360
):
352-
"""Outputs a list of all devices."""
361+
"""Get information about many devices and their backup sets."""
362+
if inactive:
363+
active = False
353364
columns = ["guid", "userUid"]
354-
devices_dataframe = _get_device_dataframe(state.sdk, columns, active, org_uid)
365+
df = _get_device_dataframe(state.sdk, columns, active, org_uid)
355366
if include_usernames:
356-
devices_dataframe = _add_usernames_to_device_dataframe(
357-
state.sdk, devices_dataframe
358-
)
359-
devices_dataframe = _add_backup_set_settings_to_dataframe(
360-
state.sdk, devices_dataframe
361-
)
362-
formatter = DataFrameOutputFormatter(format)
363-
formatter.echo_formatted_dataframe(devices_dataframe)
367+
df = _add_usernames_to_device_dataframe(state.sdk, df)
368+
df = _add_backup_set_settings_to_dataframe(state.sdk, df)
369+
if df.empty:
370+
click.echo("No results found.")
371+
else:
372+
formatter = DataFrameOutputFormatter(format)
373+
formatter.echo_formatted_dataframe(df)
364374

365375

366376
def _add_backup_set_settings_to_dataframe(sdk, devices_dataframe):
@@ -414,17 +424,14 @@ def bulk(state):
414424
pass
415425

416426

417-
@bulk.command(
418-
name="deactivate",
419-
help="""Deactivate all devices on the given list.
420-
Takes as input a CSV with a 'guid' column.""",
421-
)
427+
@bulk.command(name="deactivate")
422428
@read_csv_arg(headers=["guid"])
423429
@change_device_name_option
424430
@purge_date_option
425431
@format_option
426432
@sdk_options()
427433
def bulk_deactivate(state, csv_rows, change_device_name, purge_date, format):
434+
"""Deactivate all devices from the provided CSV containing a 'guid' column."""
428435
sdk = state.sdk
429436
csv_rows[0]["deactivated"] = False
430437
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})

0 commit comments

Comments
 (0)