workspaces-euc-mcp-server 0.1.1__py3-none-any.whl
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.
- workspaces_euc_mcp_server/__init__.py +6 -0
- workspaces_euc_mcp_server/clients.py +101 -0
- workspaces_euc_mcp_server/consts.py +154 -0
- workspaces_euc_mcp_server/models.py +333 -0
- workspaces_euc_mcp_server/server.py +129 -0
- workspaces_euc_mcp_server/tools/__init__.py +4 -0
- workspaces_euc_mcp_server/tools/_common.py +87 -0
- workspaces_euc_mcp_server/tools/cost.py +314 -0
- workspaces_euc_mcp_server/tools/destructive.py +307 -0
- workspaces_euc_mcp_server/tools/diagnostics.py +799 -0
- workspaces_euc_mcp_server/tools/inventory.py +158 -0
- workspaces_euc_mcp_server/tools/lifecycle.py +564 -0
- workspaces_euc_mcp_server/tools/performance.py +620 -0
- workspaces_euc_mcp_server/tools/pricing.py +152 -0
- workspaces_euc_mcp_server/tools/reporting.py +529 -0
- workspaces_euc_mcp_server/tools/secure_browser.py +190 -0
- workspaces_euc_mcp_server-0.1.1.dist-info/METADATA +270 -0
- workspaces_euc_mcp_server-0.1.1.dist-info/RECORD +21 -0
- workspaces_euc_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- workspaces_euc_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
- workspaces_euc_mcp_server-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Copyright bengroeneveldsg. Licensed under the Apache License, Version 2.0 (the "License").
|
|
2
|
+
# You may not use this file except in compliance with the License.
|
|
3
|
+
# A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
+
"""MCP server for administering the Amazon WorkSpaces End User Computing portfolio."""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Copyright bengroeneveldsg. Licensed under the Apache License, Version 2.0 (the "License").
|
|
2
|
+
# You may not use this file except in compliance with the License.
|
|
3
|
+
# A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
+
"""Region/profile-aware boto3 client factory.
|
|
5
|
+
|
|
6
|
+
Clients are built from the standard boto3 credential chain (``AWS_PROFILE`` / ``AWS_REGION`` /
|
|
7
|
+
instance role / SSO). For multi-account / MSP use, pass ``role_arn`` (and optional ``external_id``)
|
|
8
|
+
to transparently ``sts:AssumeRole`` into another account — the assumed credentials auto-refresh
|
|
9
|
+
before expiry, and no tool code changes (every tool just calls ``factory.client(...)``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import boto3
|
|
17
|
+
from botocore.config import Config
|
|
18
|
+
from botocore.credentials import RefreshableCredentials
|
|
19
|
+
from botocore.session import get_session as _botocore_get_session
|
|
20
|
+
|
|
21
|
+
from . import consts
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ClientFactory:
|
|
25
|
+
"""Builds and caches boto3 clients for the in-scope EUC services."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
region: str | None = None,
|
|
30
|
+
profile: str | None = None,
|
|
31
|
+
role_arn: str | None = None,
|
|
32
|
+
external_id: str | None = None,
|
|
33
|
+
role_session_name: str = "workspaces-euc-mcp-server",
|
|
34
|
+
) -> None:
|
|
35
|
+
self._region = region
|
|
36
|
+
self._profile = profile
|
|
37
|
+
self._role_arn = role_arn
|
|
38
|
+
self._external_id = external_id
|
|
39
|
+
self._role_session_name = role_session_name
|
|
40
|
+
# The base session holds the caller's own credentials (profile / env / SSO / instance role).
|
|
41
|
+
self._base_session = boto3.Session(profile_name=profile, region_name=region)
|
|
42
|
+
# The effective session: the base one, or a cross-account assumed-role one (MSP / multi-
|
|
43
|
+
# account). Assumed credentials auto-refresh before expiry, so clients keep working.
|
|
44
|
+
self._session = self._build_assumed_session(role_arn) if role_arn else self._base_session
|
|
45
|
+
self._cache: dict[tuple[str, str | None], Any] = {}
|
|
46
|
+
|
|
47
|
+
def _build_assumed_session(self, role_arn: str) -> boto3.Session:
|
|
48
|
+
"""Build a boto3 Session backed by auto-refreshing sts:AssumeRole credentials."""
|
|
49
|
+
|
|
50
|
+
external_id = self._external_id
|
|
51
|
+
session_name = self._role_session_name
|
|
52
|
+
|
|
53
|
+
def _refresh() -> dict[str, str]:
|
|
54
|
+
sts = self._base_session.client("sts", config=self._config())
|
|
55
|
+
kwargs: dict[str, str] = {"RoleArn": role_arn, "RoleSessionName": session_name}
|
|
56
|
+
if external_id:
|
|
57
|
+
kwargs["ExternalId"] = external_id
|
|
58
|
+
creds = sts.assume_role(**kwargs)["Credentials"]
|
|
59
|
+
return {
|
|
60
|
+
"access_key": creds["AccessKeyId"],
|
|
61
|
+
"secret_key": creds["SecretAccessKey"],
|
|
62
|
+
"token": creds["SessionToken"],
|
|
63
|
+
"expiry_time": creds["Expiration"].isoformat(),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
refreshable = RefreshableCredentials.create_from_metadata(
|
|
67
|
+
metadata=_refresh(),
|
|
68
|
+
refresh_using=_refresh,
|
|
69
|
+
method="sts-assume-role",
|
|
70
|
+
)
|
|
71
|
+
botocore_session = _botocore_get_session()
|
|
72
|
+
botocore_session._credentials = refreshable
|
|
73
|
+
if self._region:
|
|
74
|
+
botocore_session.set_config_variable("region", self._region)
|
|
75
|
+
return boto3.Session(botocore_session=botocore_session)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def region(self) -> str | None:
|
|
79
|
+
"""Effective region (explicit override, else whatever the base session resolved)."""
|
|
80
|
+
return self._region or self._base_session.region_name
|
|
81
|
+
|
|
82
|
+
def _config(self) -> Config:
|
|
83
|
+
return Config(
|
|
84
|
+
user_agent_extra=f"{consts.SERVER_NAME}/{consts.SERVER_VERSION}",
|
|
85
|
+
retries={"max_attempts": 3, "mode": "standard"},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def client(self, service_name: str, region: str | None = None) -> Any:
|
|
89
|
+
"""Return a cached boto3 client for ``service_name`` in the target region.
|
|
90
|
+
|
|
91
|
+
Typed ``Any`` because boto3 clients are dynamically generated per service.
|
|
92
|
+
"""
|
|
93
|
+
target_region = region or self._region
|
|
94
|
+
key = (service_name, target_region)
|
|
95
|
+
if key not in self._cache:
|
|
96
|
+
self._cache[key] = self._session.client(
|
|
97
|
+
service_name,
|
|
98
|
+
region_name=target_region,
|
|
99
|
+
config=self._config(),
|
|
100
|
+
)
|
|
101
|
+
return self._cache[key]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Copyright bengroeneveldsg. Licensed under the Apache License, Version 2.0 (the "License").
|
|
2
|
+
# You may not use this file except in compliance with the License.
|
|
3
|
+
# A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
+
"""Constants for the WorkSpaces EUC MCP server.
|
|
5
|
+
|
|
6
|
+
Note the distinction between **product names** (current official AWS branding, used everywhere a
|
|
7
|
+
human reads) and **API identifiers** (the legacy boto3 client names the SDK still requires).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
|
|
12
|
+
SERVER_NAME = "workspaces-euc-mcp-server"
|
|
13
|
+
SERVER_VERSION = __version__
|
|
14
|
+
|
|
15
|
+
# boto3 client names (API identifiers — NOT product names).
|
|
16
|
+
WORKSPACES_API = "workspaces" # Amazon WorkSpaces Personal, Pools, and Core all use this client.
|
|
17
|
+
APPSTREAM_API = "appstream" # Amazon WorkSpaces Applications (formerly AppStream 2.0).
|
|
18
|
+
SECURE_BROWSER_API = "workspaces-web" # Amazon WorkSpaces Secure Browser (formerly WorkSpaces Web).
|
|
19
|
+
WORKSPACES_INSTANCES_API = "workspaces-instances" # Amazon WorkSpaces Core Managed Instances.
|
|
20
|
+
DIRECTORY_API = "ds" # AWS Directory Service (shared dependency).
|
|
21
|
+
CLOUDWATCH_API = "cloudwatch" # Telemetry for diagnostics/cost tools.
|
|
22
|
+
EC2_API = "ec2" # Used to enrich WorkSpaces Core Managed Instances with EC2 details.
|
|
23
|
+
COST_EXPLORER_API = "ce" # Cost Explorer (global; account-wide, not region-scoped).
|
|
24
|
+
PRICING_API = "pricing" # AWS Price List (global).
|
|
25
|
+
|
|
26
|
+
# Cost Explorer is a global endpoint served from us-east-1 regardless of the working region.
|
|
27
|
+
COST_EXPLORER_REGION = "us-east-1"
|
|
28
|
+
|
|
29
|
+
# Cost Explorer SERVICE dimension values that map to the EUC portfolio. Names can vary by
|
|
30
|
+
# account/era; the filter simply ignores values that produce no results.
|
|
31
|
+
EUC_COST_EXPLORER_SERVICES = [
|
|
32
|
+
"Amazon WorkSpaces",
|
|
33
|
+
"Amazon AppStream",
|
|
34
|
+
"Amazon WorkSpaces Web",
|
|
35
|
+
"Amazon WorkSpaces Secure Browser",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Current official product names (used in all human-facing output).
|
|
39
|
+
PRODUCT_WORKSPACES_PERSONAL = "Amazon WorkSpaces Personal"
|
|
40
|
+
PRODUCT_WORKSPACES_POOLS = "Amazon WorkSpaces Pools"
|
|
41
|
+
PRODUCT_WORKSPACES_APPLICATIONS = "Amazon WorkSpaces Applications"
|
|
42
|
+
PRODUCT_SECURE_BROWSER = "Amazon WorkSpaces Secure Browser"
|
|
43
|
+
PRODUCT_WORKSPACES_CORE_INSTANCES = "Amazon WorkSpaces Core Managed Instances"
|
|
44
|
+
|
|
45
|
+
# Legacy / former product names mapped to their current official name. Accept these as INPUT
|
|
46
|
+
# (users will keep saying them) but always emit the current name in output. This is surfaced to the
|
|
47
|
+
# MCP client model via the server instructions and tool descriptions so a query about, say,
|
|
48
|
+
# "AppStream fleets" routes to the WorkSpaces Applications tools.
|
|
49
|
+
LEGACY_NAME_ALIASES = {
|
|
50
|
+
"appstream": PRODUCT_WORKSPACES_APPLICATIONS,
|
|
51
|
+
"appstream 2.0": PRODUCT_WORKSPACES_APPLICATIONS,
|
|
52
|
+
"amazon appstream": PRODUCT_WORKSPACES_APPLICATIONS,
|
|
53
|
+
"amazon appstream 2.0": PRODUCT_WORKSPACES_APPLICATIONS,
|
|
54
|
+
"workspaces web": PRODUCT_SECURE_BROWSER,
|
|
55
|
+
"amazon workspaces web": PRODUCT_SECURE_BROWSER,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Default blast-radius cap for any (future, Phase 2) bulk mutation.
|
|
59
|
+
DEFAULT_MAX_BULK_TARGETS = 25
|
|
60
|
+
|
|
61
|
+
# WorkSpaces Personal general-purpose compute types, smallest -> largest. Used by bundle
|
|
62
|
+
# right-sizing to step a desktop up/down one size. Graphics families are intentionally excluded
|
|
63
|
+
# (different hardware/pricing; not safe to auto-suggest across).
|
|
64
|
+
WORKSPACES_COMPUTE_ORDER = ["VALUE", "STANDARD", "PERFORMANCE", "POWER", "POWERPRO"]
|
|
65
|
+
|
|
66
|
+
# Performance metrics published natively to the AWS/WorkSpaces namespace (keyed by WorkspaceId),
|
|
67
|
+
# with a best-effort unit label. No CloudWatch agent is required for these.
|
|
68
|
+
WORKSPACES_PERFORMANCE_METRICS = [
|
|
69
|
+
("CPUUsage", "Percent"),
|
|
70
|
+
("MemoryUsage", "Percent"),
|
|
71
|
+
("GPUUsage", "Percent"),
|
|
72
|
+
("FramesPerSecond", "Count"),
|
|
73
|
+
("RootVolumeDiskUsage", "Percent"),
|
|
74
|
+
("UserVolumeDiskUsage", "Percent"),
|
|
75
|
+
("InSessionLatency", "Milliseconds"),
|
|
76
|
+
("UpTime", "Seconds"),
|
|
77
|
+
("Bandwidth", "Bytes"),
|
|
78
|
+
("BandwidthInbound", "Bytes"),
|
|
79
|
+
("CPUQueueLength", "Count"),
|
|
80
|
+
("MemoryPageHardFaults", "Count"),
|
|
81
|
+
("RootVolumeDiskIOQueueLength", "Count"),
|
|
82
|
+
("UserVolumeDiskIOQueueLength", "Count"),
|
|
83
|
+
("TCPRetransmissionRate", "Percent"),
|
|
84
|
+
("UDPPacketLossRate", "Percent"),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# Capacity/utilization metrics published to AWS/AppStream for a fleet (dimension Fleet), used for
|
|
88
|
+
# WorkSpaces Applications fleet usage history. (ActiveSessions etc. only exist for elastic /
|
|
89
|
+
# multi-session fleets, so they are not in the base set.)
|
|
90
|
+
APPSTREAM_FLEET_METRICS = [
|
|
91
|
+
("InUseCapacity", "Count"),
|
|
92
|
+
("CapacityUtilization", "Percent"),
|
|
93
|
+
("ActualCapacity", "Count"),
|
|
94
|
+
("AvailableCapacity", "Count"),
|
|
95
|
+
("RunningCapacity", "Count"),
|
|
96
|
+
("DesiredCapacity", "Count"),
|
|
97
|
+
("PendingCapacity", "Count"),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
# WorkSpaces Personal connection/session metrics (AWS/WorkSpaces, dimension WorkspaceId). Idle
|
|
101
|
+
# desktops typically publish only UserConnected; the rest emit when there are connection attempts.
|
|
102
|
+
WORKSPACES_CONNECTION_METRICS = [
|
|
103
|
+
("UserConnected", "Count"),
|
|
104
|
+
("ConnectionAttempt", "Count"),
|
|
105
|
+
("ConnectionSuccess", "Count"),
|
|
106
|
+
("ConnectionFailure", "Count"),
|
|
107
|
+
("SessionLaunchTime", "Milliseconds"),
|
|
108
|
+
("InSessionLatency", "Milliseconds"),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# WorkSpaces Pools user-session metrics. NOTE the dimension name is literally "WorkSpaces pool ID"
|
|
112
|
+
# (with spaces), not PoolId — verified against a live account.
|
|
113
|
+
WORKSPACES_POOL_DIMENSION = "WorkSpaces pool ID"
|
|
114
|
+
WORKSPACES_POOL_SESSION_METRICS = [
|
|
115
|
+
("ActiveUserSessionCapacity", "Count"),
|
|
116
|
+
("ActualUserSessionCapacity", "Count"),
|
|
117
|
+
("AvailableUserSessionCapacity", "Count"),
|
|
118
|
+
("DesiredUserSessionCapacity", "Count"),
|
|
119
|
+
("PendingUserSessionCapacity", "Count"),
|
|
120
|
+
("UserSessionsCapacityUtilization", "Percent"),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Secure Browser session metrics (AWS/WorkSpacesWeb, dimension PortalId). NOTE: unlike the other
|
|
124
|
+
# services, Secure Browser only emits these when sessions actually occur (idle portals publish
|
|
125
|
+
# nothing), and richer usage goes via the Session Logger. Names below are per AWS docs and are
|
|
126
|
+
# NOT yet verified against live data (the account's portals have had no sessions).
|
|
127
|
+
SECURE_BROWSER_NAMESPACE = "AWS/WorkSpacesWeb"
|
|
128
|
+
SECURE_BROWSER_PORTAL_DIMENSION = "PortalId"
|
|
129
|
+
SECURE_BROWSER_SESSION_METRICS = [
|
|
130
|
+
("SessionAttempt", "Count"),
|
|
131
|
+
("SessionSuccess", "Count"),
|
|
132
|
+
("SessionFailure", "Count"),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# Secure Browser user-settings flags that, when "Enabled", relax data-egress controls. Used by the
|
|
136
|
+
# security audit. Verified live: GetUserSettings returns these as "Enabled"/"Disabled".
|
|
137
|
+
SECURE_BROWSER_EGRESS_FLAGS = ["downloadAllowed", "copyAllowed", "printAllowed"]
|
|
138
|
+
|
|
139
|
+
SERVER_INSTRUCTIONS = """\
|
|
140
|
+
Administrator-focused MCP server for the Amazon WorkSpaces End User Computing portfolio:
|
|
141
|
+
WorkSpaces Personal, WorkSpaces Pools, WorkSpaces Applications, WorkSpaces Secure Browser, and
|
|
142
|
+
WorkSpaces Core. Tools are read-only by default and synthesize cross-service results for
|
|
143
|
+
inventory, troubleshooting, and cost/utilization optimization. Write/lifecycle operations are not
|
|
144
|
+
enabled unless the server was launched with --enable-writes and the matching IAM permissions are
|
|
145
|
+
present.
|
|
146
|
+
|
|
147
|
+
Legacy/former service names are fully supported as input — accept them and treat them as the
|
|
148
|
+
current service, but ALWAYS use the current official name in your response:
|
|
149
|
+
- "AppStream", "AppStream 2.0", "Amazon AppStream 2.0" -> Amazon WorkSpaces Applications
|
|
150
|
+
- "WorkSpaces Web" -> Amazon WorkSpaces Secure Browser
|
|
151
|
+
Amazon WorkSpaces Applications IS the rebranded AppStream 2.0 (same service and API), so a request
|
|
152
|
+
about "AppStream fleets" or "AppStream stacks" is about WorkSpaces Applications and is handled by
|
|
153
|
+
the application-fleet tools — do not say AppStream is unsupported.
|
|
154
|
+
"""
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Copyright bengroeneveldsg. Licensed under the Apache License, Version 2.0 (the "License").
|
|
2
|
+
# You may not use this file except in compliance with the License.
|
|
3
|
+
# A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
+
"""Pydantic models for tool inputs and synthesized outputs."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ServiceInventory(BaseModel):
|
|
12
|
+
"""Per-service rollup within an inventory summary."""
|
|
13
|
+
|
|
14
|
+
service: str = Field(description="Current official AWS product name.")
|
|
15
|
+
resource_type: str = Field(description="The kind of resource counted (e.g. WorkSpace, Fleet).")
|
|
16
|
+
count: int = Field(description="Number of resources of this type found in the region.")
|
|
17
|
+
by_state: dict[str, int] = Field(
|
|
18
|
+
default_factory=dict,
|
|
19
|
+
description="Breakdown of resources by their lifecycle state.",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ServiceError(BaseModel):
|
|
24
|
+
"""A non-fatal error encountered while calling one AWS service.
|
|
25
|
+
|
|
26
|
+
Collection/diagnosis is best-effort: a permission gap or unsupported region for one service is
|
|
27
|
+
recorded here rather than failing the whole result.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
service: str
|
|
31
|
+
operation: str = Field(description="The AWS API operation that failed.")
|
|
32
|
+
message: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Backwards-compatible alias (the inventory tool predates the generic name).
|
|
36
|
+
InventoryError = ServiceError
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EucInventorySummary(BaseModel):
|
|
40
|
+
"""Cross-service inventory of the Amazon WorkSpaces EUC portfolio in one region."""
|
|
41
|
+
|
|
42
|
+
region: str | None = Field(description="AWS region the summary covers.")
|
|
43
|
+
account_scope: str = Field(default="single-account")
|
|
44
|
+
total_resources: int = Field(description="Sum of counts across all services collected.")
|
|
45
|
+
services: list[ServiceInventory] = Field(default_factory=list)
|
|
46
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
47
|
+
notes: list[str] = Field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Finding(BaseModel):
|
|
51
|
+
"""A single observation from a diagnostic or audit tool."""
|
|
52
|
+
|
|
53
|
+
severity: str = Field(description="One of: info, warning, critical.")
|
|
54
|
+
title: str = Field(description="Short human-readable headline.")
|
|
55
|
+
detail: str = Field(description="What was observed and why it matters.")
|
|
56
|
+
recommendation: str | None = Field(default=None, description="Suggested next action, if any.")
|
|
57
|
+
resource_id: str | None = Field(
|
|
58
|
+
default=None, description="Resource the finding applies to, when relevant."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Diagnosis(BaseModel):
|
|
63
|
+
"""Synthesized diagnosis for a single EUC resource."""
|
|
64
|
+
|
|
65
|
+
target_type: str = Field(description="Kind of resource diagnosed (official product name).")
|
|
66
|
+
target_id: str = Field(description="Identifier of the resource diagnosed.")
|
|
67
|
+
region: str | None = None
|
|
68
|
+
status: str = Field(
|
|
69
|
+
description="Overall verdict: healthy, degraded, unhealthy, unknown, or not_found."
|
|
70
|
+
)
|
|
71
|
+
summary: str = Field(description="One-line plain-language verdict.")
|
|
72
|
+
signals: dict[str, object] = Field(
|
|
73
|
+
default_factory=dict,
|
|
74
|
+
description="Key raw signals observed (state, capacity, connection status, etc.).",
|
|
75
|
+
)
|
|
76
|
+
findings: list[Finding] = Field(default_factory=list)
|
|
77
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DirectoryHealthReport(BaseModel):
|
|
81
|
+
"""Health of one or more WorkSpaces-registered directories in a region."""
|
|
82
|
+
|
|
83
|
+
region: str | None = None
|
|
84
|
+
directories: list[Diagnosis] = Field(default_factory=list)
|
|
85
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class MetricStat(BaseModel):
|
|
89
|
+
"""Aggregated values for one CloudWatch metric over the window."""
|
|
90
|
+
|
|
91
|
+
latest: float | None = None
|
|
92
|
+
average: float | None = None
|
|
93
|
+
peak: float | None = None
|
|
94
|
+
unit: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class WorkspacePerformance(BaseModel):
|
|
98
|
+
"""Native AWS/WorkSpaces performance metrics for one desktop."""
|
|
99
|
+
|
|
100
|
+
workspace_id: str
|
|
101
|
+
lookback_hours: int
|
|
102
|
+
metrics: dict[str, MetricStat] = Field(default_factory=dict)
|
|
103
|
+
note: str | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PerformanceReport(BaseModel):
|
|
107
|
+
"""Performance metrics across one or more WorkSpaces Personal desktops."""
|
|
108
|
+
|
|
109
|
+
region: str | None = None
|
|
110
|
+
lookback_hours: int
|
|
111
|
+
workspaces: list[WorkspacePerformance] = Field(default_factory=list)
|
|
112
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
113
|
+
notes: list[str] = Field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class UsagePoint(BaseModel):
|
|
117
|
+
"""One time-bucket in a usage time-series."""
|
|
118
|
+
|
|
119
|
+
timestamp: str
|
|
120
|
+
average: float | None = None
|
|
121
|
+
peak: float | None = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class FleetMetricSeries(BaseModel):
|
|
125
|
+
"""Aggregates plus the per-bucket time-series for one fleet metric."""
|
|
126
|
+
|
|
127
|
+
unit: str
|
|
128
|
+
latest: float | None = None
|
|
129
|
+
average: float | None = None
|
|
130
|
+
peak: float | None = None
|
|
131
|
+
series: list[UsagePoint] = Field(default_factory=list)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class FleetUsage(BaseModel):
|
|
135
|
+
"""Usage history for a WorkSpaces Applications fleet over a window."""
|
|
136
|
+
|
|
137
|
+
fleet_name: str
|
|
138
|
+
lookback_days: int
|
|
139
|
+
period_hours: int
|
|
140
|
+
metrics: dict[str, FleetMetricSeries] = Field(default_factory=dict)
|
|
141
|
+
summary: str | None = None
|
|
142
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SecureBrowserPortalDetails(BaseModel):
|
|
146
|
+
"""Resolved settings for a WorkSpaces Secure Browser portal."""
|
|
147
|
+
|
|
148
|
+
portal_arn: str
|
|
149
|
+
display_name: str | None = None
|
|
150
|
+
authentication_type: str | None = None
|
|
151
|
+
status: str | None = None
|
|
152
|
+
user_settings: dict[str, object] = Field(
|
|
153
|
+
default_factory=dict,
|
|
154
|
+
description="Clipboard/print/download/upload controls and timeouts.",
|
|
155
|
+
)
|
|
156
|
+
network: dict[str, object] = Field(default_factory=dict)
|
|
157
|
+
has_browser_policy: bool = False
|
|
158
|
+
has_data_protection: bool = False
|
|
159
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class UsageHistory(BaseModel):
|
|
163
|
+
"""Generic metric time-series history for a single EUC resource over a window."""
|
|
164
|
+
|
|
165
|
+
target_type: str = Field(description="Resource kind, e.g. the current official product name.")
|
|
166
|
+
target_id: str
|
|
167
|
+
lookback_days: int
|
|
168
|
+
period_hours: int
|
|
169
|
+
metrics: dict[str, FleetMetricSeries] = Field(default_factory=dict)
|
|
170
|
+
summary: str | None = None
|
|
171
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class WorkspaceUtilization(BaseModel):
|
|
175
|
+
"""Utilization classification for a single WorkSpaces Personal desktop."""
|
|
176
|
+
|
|
177
|
+
workspace_id: str
|
|
178
|
+
running_mode: str | None = None
|
|
179
|
+
lookback_days: int
|
|
180
|
+
active_days: int | None = Field(
|
|
181
|
+
default=None, description="Days with at least one user connection in the window."
|
|
182
|
+
)
|
|
183
|
+
classification: str = Field(description="unused, idle, active, or unknown.")
|
|
184
|
+
# Inputs for pricing/savings estimates (not always populated).
|
|
185
|
+
compute_type: str | None = None
|
|
186
|
+
operating_system: str | None = None
|
|
187
|
+
root_volume_gib: int | None = None
|
|
188
|
+
user_volume_gib: int | None = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class UtilizationReport(BaseModel):
|
|
192
|
+
"""Utilization rollup across WorkSpaces Personal desktops in a region."""
|
|
193
|
+
|
|
194
|
+
region: str | None = None
|
|
195
|
+
lookback_days: int
|
|
196
|
+
total: int
|
|
197
|
+
counts: dict[str, int] = Field(
|
|
198
|
+
default_factory=dict, description="Count of desktops per classification."
|
|
199
|
+
)
|
|
200
|
+
workspaces: list[WorkspaceUtilization] = Field(default_factory=list)
|
|
201
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
202
|
+
notes: list[str] = Field(default_factory=list)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class Recommendation(BaseModel):
|
|
206
|
+
"""A single cost/utilization optimization recommendation."""
|
|
207
|
+
|
|
208
|
+
target_id: str
|
|
209
|
+
kind: str = Field(description="The recommendation type, e.g. running_mode.")
|
|
210
|
+
current: str | None = None
|
|
211
|
+
recommended: str | None = None
|
|
212
|
+
rationale: str
|
|
213
|
+
estimated_monthly_savings_usd: float | None = Field(
|
|
214
|
+
default=None, description="Estimated saving, when it can be computed; otherwise null."
|
|
215
|
+
)
|
|
216
|
+
confidence: str = Field(description="low, medium, or high.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class RecommendationReport(BaseModel):
|
|
220
|
+
"""Set of optimization recommendations for a region."""
|
|
221
|
+
|
|
222
|
+
region: str | None = None
|
|
223
|
+
lookback_days: int
|
|
224
|
+
recommendations: list[Recommendation] = Field(default_factory=list)
|
|
225
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
226
|
+
notes: list[str] = Field(default_factory=list)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class CostLineItem(BaseModel):
|
|
230
|
+
service: str
|
|
231
|
+
amount: float
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class CostSummary(BaseModel):
|
|
235
|
+
"""Cost rollup for the EUC portfolio over a time window (account-wide)."""
|
|
236
|
+
|
|
237
|
+
scope: str = Field(
|
|
238
|
+
default="account-wide",
|
|
239
|
+
description="Cost Explorer is not region-scoped; figures are account-wide.",
|
|
240
|
+
)
|
|
241
|
+
start: str
|
|
242
|
+
end: str
|
|
243
|
+
granularity: str
|
|
244
|
+
currency: str = "USD"
|
|
245
|
+
total: float
|
|
246
|
+
by_service: list[CostLineItem] = Field(default_factory=list)
|
|
247
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
248
|
+
notes: list[str] = Field(default_factory=list)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ResourceRecord(BaseModel):
|
|
252
|
+
"""A single resource row in an inventory report."""
|
|
253
|
+
|
|
254
|
+
id: str
|
|
255
|
+
name: str | None = None
|
|
256
|
+
state: str | None = None
|
|
257
|
+
attributes: dict[str, object] = Field(default_factory=dict)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class InventoryReportSection(BaseModel):
|
|
261
|
+
service: str
|
|
262
|
+
resource_type: str
|
|
263
|
+
resources: list[ResourceRecord] = Field(default_factory=list)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class InventoryReport(BaseModel):
|
|
267
|
+
"""Detailed per-resource inventory across the EUC portfolio in a region."""
|
|
268
|
+
|
|
269
|
+
region: str | None = None
|
|
270
|
+
total_resources: int = 0
|
|
271
|
+
sections: list[InventoryReportSection] = Field(default_factory=list)
|
|
272
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class AuditReport(BaseModel):
|
|
276
|
+
"""Security-posture findings across the EUC portfolio in a region."""
|
|
277
|
+
|
|
278
|
+
region: str | None = None
|
|
279
|
+
findings: list[Finding] = Field(default_factory=list)
|
|
280
|
+
severity_counts: dict[str, int] = Field(default_factory=dict)
|
|
281
|
+
resources_checked: dict[str, int] = Field(default_factory=dict)
|
|
282
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class UnusedResource(BaseModel):
|
|
286
|
+
service: str
|
|
287
|
+
resource_type: str
|
|
288
|
+
id: str
|
|
289
|
+
reason: str
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class UnusedResourcesReport(BaseModel):
|
|
293
|
+
"""Candidate idle/unused resources worth reclaiming."""
|
|
294
|
+
|
|
295
|
+
region: str | None = None
|
|
296
|
+
lookback_days: int
|
|
297
|
+
items: list[UnusedResource] = Field(default_factory=list)
|
|
298
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
299
|
+
notes: list[str] = Field(default_factory=list)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TargetResult(BaseModel):
|
|
303
|
+
"""Outcome of a write action against a single target."""
|
|
304
|
+
|
|
305
|
+
target_id: str
|
|
306
|
+
status: str = Field(description="ok, error, or skipped.")
|
|
307
|
+
message: str | None = None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class WriteOutcome(BaseModel):
|
|
311
|
+
"""Result of a guarded write/lifecycle action.
|
|
312
|
+
|
|
313
|
+
Mutations are dry-run by default: unless ``confirmed`` is true the action only reports the plan
|
|
314
|
+
and changes nothing. Bulk actions are refused when the target count exceeds the blast-radius
|
|
315
|
+
cap.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
action: str = Field(description="The lifecycle action requested.")
|
|
319
|
+
dry_run: bool = Field(description="True when nothing was changed (plan only).")
|
|
320
|
+
confirmed: bool = Field(description="Whether the caller explicitly confirmed execution.")
|
|
321
|
+
requested_targets: list[str] = Field(default_factory=list)
|
|
322
|
+
max_bulk_targets: int = Field(description="Configured blast-radius cap.")
|
|
323
|
+
blast_radius_ok: bool = Field(
|
|
324
|
+
description="False when the action was refused for being too large."
|
|
325
|
+
)
|
|
326
|
+
plan: str = Field(description="Human-readable description of what would happen / happened.")
|
|
327
|
+
acknowledgement_required: str | None = Field(
|
|
328
|
+
default=None,
|
|
329
|
+
description="For destructive actions: the exact phrase the caller must pass to proceed.",
|
|
330
|
+
)
|
|
331
|
+
results: list[TargetResult] = Field(default_factory=list)
|
|
332
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
333
|
+
notes: list[str] = Field(default_factory=list)
|