1010from .audit import AuditEvent , Auditor
1111from .authz import Authorizer , CallerContext , TokenBucketLimiter , set_caller
1212from .config import AppConfig , load_config
13- from .vsphere_client import VsphereClient
14-
15-
16- def _choose_host (cfg : AppConfig , hostname : Optional [str ]) -> str :
17- host = (hostname or cfg .vsphere .host ).strip ()
18- if cfg .vsphere .allowed_hosts and host not in cfg .vsphere .allowed_hosts :
19- raise PermissionError (f"Hostname '{ host } ' not in allowed set" )
20- return host
13+ from .vsphere_client import VsphereClientPool
2114
2215
2316def _with_guard (tool_name : str , destructive : bool = False ):
@@ -28,6 +21,7 @@ def wrapper(*args, **kwargs):
2821 auditor : Auditor = kwargs .pop ("_auditor" )
2922 limiter : TokenBucketLimiter = kwargs .pop ("_limiter" )
3023 authz : Authorizer = kwargs .pop ("_authz" )
24+ pool : VsphereClientPool = kwargs .pop ("_pool" )
3125
3226 token = kwargs .pop ("token" , None )
3327 confirm = kwargs .get ("confirm" , False )
@@ -50,7 +44,7 @@ def wrapper(*args, **kwargs):
5044 if destructive and not confirm :
5145 raise PermissionError ("Destructive operation: set confirm=True to proceed" )
5246
53- result = fn (* args , ** kwargs )
47+ result = fn (* args , _pool = pool , ** kwargs )
5448 ok = True
5549 if isinstance (result , dict ) and "meta" in result and "host" in result ["meta" ]:
5650 host_used = result ["meta" ]["host" ]
@@ -73,128 +67,159 @@ def build_server(cfg: AppConfig) -> FastMCP:
7367 auditor = Auditor (cfg .server .audit_log_path )
7468 authz = Authorizer (cfg .auth )
7569 limiter = TokenBucketLimiter (cfg .ratelimit )
76-
77- def inject (fn , name : str , destructive : bool = False ):
78- wrapped = _with_guard (name , destructive )(fn )
79- mcp .tool (name = name )(functools .partial (wrapped , _cfg = cfg , _auditor = auditor , _limiter = limiter , _authz = authz ))
80-
81- def client_for (hostname : Optional [str ]) -> VsphereClient :
82- host = _choose_host (cfg , hostname )
83- local_cfg = cfg .model_copy (deep = True )
84- local_cfg .vsphere .host = host
85- c = VsphereClient (local_cfg .vsphere )
86- c .login ()
87- return c
88-
89- @inject
90- def list_vms (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
91- c = client_for (hostname )
70+ pool = VsphereClientPool (cfg )
71+
72+ def inject (name : str , destructive : bool = False ):
73+ """Decorator factory that wraps a tool with auth, rate limiting, and auditing."""
74+ def decorator (fn ):
75+ wrapped = _with_guard (name , destructive )(fn )
76+ bound = functools .partial (
77+ wrapped ,
78+ _cfg = cfg ,
79+ _auditor = auditor ,
80+ _limiter = limiter ,
81+ _authz = authz ,
82+ _pool = pool ,
83+ )
84+ mcp .tool (name = name )(bound )
85+ return fn
86+ return decorator
87+
88+ # --- VM Discovery ---
89+
90+ @inject ("list_vms" )
91+ def list_vms (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
92+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
93+ c = _pool .get (hostname )
9294 vms = c .list_vms ()
93- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "count" : len (vms ), "vms" : vms }
95+ return {"ok" : True , "meta" : {"host" : c .host }, "count" : len (vms ), "vms" : vms }
9496
95- @inject
96- def get_vm_details (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
97- c = client_for (hostname )
97+ @inject ("get_vm_details" )
98+ def get_vm_details (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
99+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
100+ c = _pool .get (hostname )
98101 vm = c .get_vm (vm_id )
99- return {"ok" : True , "meta" : {"host" : c ._cfg .host }, "vm" : vm }
102+ return {"ok" : True , "meta" : {"host" : c .host }, "vm" : vm }
103+
104+ # --- Inventory Discovery ---
100105
101- @inject
102- def list_hosts (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
103- c = client_for (hostname )
106+ @inject ("list_hosts" )
107+ def list_hosts (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
108+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
109+ c = _pool .get (hostname )
104110 data = c .list_hosts ()
105- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "count" : len (data ), "hosts" : data }
111+ return {"ok" : True , "meta" : {"host" : c .host }, "count" : len (data ), "hosts" : data }
106112
107- @inject
108- def list_datastores (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
109- c = client_for (hostname )
113+ @inject ("list_datastores" )
114+ def list_datastores (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
115+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
116+ c = _pool .get (hostname )
110117 data = c .list_datastores ()
111- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "count" : len (data ), "datastores" : data }
118+ return {"ok" : True , "meta" : {"host" : c .host }, "count" : len (data ), "datastores" : data }
112119
113- @inject
114- def list_networks (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
115- c = client_for (hostname )
120+ @inject ("list_networks" )
121+ def list_networks (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
122+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
123+ c = _pool .get (hostname )
116124 data = c .list_networks ()
117- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "count" : len (data ), "networks" : data }
125+ return {"ok" : True , "meta" : {"host" : c .host }, "count" : len (data ), "networks" : data }
118126
119- @inject
120- def list_datacenters (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
121- c = client_for (hostname )
127+ @inject ("list_datacenters" )
128+ def list_datacenters (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
129+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
130+ c = _pool .get (hostname )
122131 data = c .list_datacenters ()
123- return {"ok" : True , "meta" : {"host" : c ._cfg .host }, "count" : len (data ), "datacenters" : data }
132+ return {"ok" : True , "meta" : {"host" : c .host }, "count" : len (data ), "datacenters" : data }
133+
134+ @inject ("get_datastore_usage" )
135+ def get_datastore_usage (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
136+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
137+ c = _pool .get (hostname )
138+ dss = c .list_datastores ()
139+ return {"ok" : True , "meta" : {"host" : c .host }, "count" : len (dss ), "datastores" : dss }
140+
141+ @inject ("get_resource_utilization_summary" )
142+ def get_resource_utilization_summary (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
143+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
144+ c = _pool .get (hostname )
145+ return {"ok" : True , "meta" : {"host" : c .host }, "summary" : {
146+ "vms" : len (c .list_vms ()),
147+ "hosts" : len (c .list_hosts ()),
148+ "datastores" : len (c .list_datastores ()),
149+ "networks" : len (c .list_networks ()),
150+ "datacenters" : len (c .list_datacenters ()),
151+ }}
124152
125- @inject
126- def power_on_vm (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
127- c = client_for (hostname )
153+ # --- Power Operations ---
154+
155+ @inject ("power_on_vm" )
156+ def power_on_vm (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
157+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
158+ c = _pool .get (hostname )
128159 data = c .power_start (vm_id )
129- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "result" : data }
160+ return {"ok" : True , "meta" : {"host" : c .host }, "result" : data }
130161
131- @inject
132- def power_off_vm (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
133- c = client_for (hostname )
162+ @inject ("power_off_vm" )
163+ def power_off_vm (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
164+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
165+ c = _pool .get (hostname )
134166 data = c .power_stop (vm_id )
135- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "result" : data }
167+ return {"ok" : True , "meta" : {"host" : c .host }, "result" : data }
136168
137- @inject
138- def restart_vm (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
139- c = client_for (hostname )
169+ @inject ("restart_vm" )
170+ def restart_vm (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
171+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
172+ c = _pool .get (hostname )
140173 data = c .power_reset (vm_id )
141- return {"ok" : True , "meta" : {"host" : c ._cfg .host }, "result" : data }
174+ return {"ok" : True , "meta" : {"host" : c .host }, "result" : data }
175+
176+ # --- Snapshot Operations ---
142177
143- @inject
144- def list_vm_snapshots (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
145- c = client_for (hostname )
178+ @inject ("list_vm_snapshots" )
179+ def list_vm_snapshots (vm_id : str , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
180+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
181+ c = _pool .get (hostname )
146182 data = c .list_snapshots (vm_id )
147- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "count" : (len (data ) if isinstance (data , list ) else None ), "snapshots" : data }
183+ return {"ok" : True , "meta" : {"host" : c .host }, "count" : (len (data ) if isinstance (data , list ) else None ), "snapshots" : data }
148184
149- @inject
185+ @inject ( "create_vm_snapshot" )
150186 def create_vm_snapshot (vm_id : str , snapshot_name : str , description : str = "" , memory : bool = False , quiesce : bool = False ,
151- hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
152- c = client_for (hostname )
187+ hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
188+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
189+ c = _pool .get (hostname )
153190 data = c .create_snapshot (vm_id , snapshot_name , description = description , memory = memory , quiesce = quiesce )
154- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "result" : data }
191+ return {"ok" : True , "meta" : {"host" : c .host }, "result" : data }
155192
156- @inject
193+ @inject ( "delete_vm_snapshot" , destructive = True )
157194 def delete_vm_snapshot (vm_id : str , snapshot_id : str , confirm : bool = False ,
158- hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
159- c = client_for (hostname )
195+ hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
196+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
197+ c = _pool .get (hostname )
160198 data = c .delete_snapshot (vm_id , snapshot_id )
161- return {"ok" : True , "meta" : {"host" : c ._cfg .host }, "result" : data }
199+ return {"ok" : True , "meta" : {"host" : c .host }, "result" : data }
200+
201+ # --- Destructive Operations (require confirm=True) ---
162202
163- @inject
164- def delete_vm (vm_id : str , confirm : bool = False , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
165- c = client_for (hostname )
203+ @inject ("delete_vm" , destructive = True )
204+ def delete_vm (vm_id : str , confirm : bool = False , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
205+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
206+ c = _pool .get (hostname )
166207 data = c .delete_vm (vm_id )
167- return {"ok" : True , "meta" : {"host" : c ._cfg . host }, "result" : data }
208+ return {"ok" : True , "meta" : {"host" : c .host }, "result" : data }
168209
169- @inject
210+ @inject ( "modify_vm_resources" , destructive = True )
170211 def modify_vm_resources (vm_id : str , cpu_count : Optional [int ] = None , memory_gb : Optional [int ] = None ,
171- confirm : bool = False , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
212+ confirm : bool = False , hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ,
213+ * , _pool : VsphereClientPool ) -> Dict [str , Any ]:
172214 if cpu_count is None and memory_gb is None :
173215 raise ValueError ("cpu_count or memory_gb required" )
174- c = client_for (hostname )
216+ c = _pool . get (hostname )
175217 res = {}
176218 if cpu_count is not None :
177219 res ["cpu" ] = c .set_cpu (vm_id , cpu_count )
178220 if memory_gb is not None :
179221 res ["memory" ] = c .set_memory (vm_id , int (memory_gb * 1024 ))
180- return {"ok" : True , "meta" : {"host" : c ._cfg .host }, "result" : res }
181-
182- @inject
183- def get_datastore_usage (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
184- c = client_for (hostname )
185- dss = c .list_datastores ()
186- return {"ok" : True , "meta" : {"host" : c ._cfg .host }, "count" : len (dss ), "datastores" : dss }
187-
188- @inject
189- def get_resource_utilization_summary (hostname : Optional [str ] = None , verbose : bool = False , token : Optional [str ] = None ) -> Dict [str , Any ]:
190- c = client_for (hostname )
191- return {"ok" : True , "meta" : {"host" : c ._cfg .host }, "summary" : {
192- "vms" : len (c .list_vms ()),
193- "hosts" : len (c .list_hosts ()),
194- "datastores" : len (c .list_datastores ()),
195- "networks" : len (c .list_networks ()),
196- "datacenters" : len (c .list_datacenters ()),
197- }}
222+ return {"ok" : True , "meta" : {"host" : c .host }, "result" : res }
198223
199224 return mcp
200225
0 commit comments