66import logging
77import ssl
88import urllib .request
9+ import warnings
910import weakref
1011from http .client import HTTPSConnection
1112from dateutil .parser import parse
12- from typing import Dict , List
13+ from typing import Dict , Literal , Optional , Sequence
1314
1415import macaroonbakery .bakery as bakery
1516import macaroonbakery .httpbakery as httpbakery
1819from juju .client import client
1920from juju .utils import IdQueue
2021from juju .version import CLIENT_VERSION
22+ from .facade_versions import client_facade_versions , known_unsupported_facades
2123
2224log = logging .getLogger ('juju.client.connection' )
2325
24- # Manual list of facades present in schemas + codegen which python-libjuju does not yet support
25- excluded_facades : Dict [str , List [int ]] = {
26- 'Charms' : [7 ],
27- }
28- # Please keep in alphabetical order
29- # in future this will likely be generated automatically (perhaps at runtime)
30- client_facades = {
31- 'Action' : {'versions' : [7 ]},
32- 'Admin' : {'versions' : [3 ]},
33- 'AllModelWatcher' : {'versions' : [4 ]},
34- 'AllWatcher' : {'versions' : [3 ]},
35- 'Annotations' : {'versions' : [2 ]},
36- 'Application' : {'versions' : [17 , 19 ]},
37- 'ApplicationOffers' : {'versions' : [4 ]},
38- 'Backups' : {'versions' : [3 ]},
39- 'Block' : {'versions' : [2 ]},
40- 'Bundle' : {'versions' : [6 ]},
41- 'Charms' : {'versions' : [6 ]},
42- 'Client' : {'versions' : [6 , 7 ]},
43- 'Cloud' : {'versions' : [7 ]},
44- 'Controller' : {'versions' : [11 ]},
45- 'CredentialManager' : {'versions' : [1 ]},
46- 'FirewallRules' : {'versions' : [1 ]},
47- 'HighAvailability' : {'versions' : [2 ]},
48- 'ImageMetadataManager' : {'versions' : [1 ]},
49- 'KeyManager' : {'versions' : [1 ]},
50- 'MachineManager' : {'versions' : [10 ]},
51- 'MetricsDebug' : {'versions' : [2 ]},
52- 'ModelConfig' : {'versions' : [3 ]},
53- 'ModelGeneration' : {'versions' : [4 ]},
54- 'ModelManager' : {'versions' : [9 ]},
55- 'ModelUpgrader' : {'versions' : [1 ]},
56- 'Payloads' : {'versions' : [1 ]},
57- 'Pinger' : {'versions' : [1 ]},
58- 'Resources' : {'versions' : [3 ]},
59- 'SSHClient' : {'versions' : [4 ]},
60- 'SecretBackends' : {'versions' : [1 ]},
61- 'Secrets' : {'versions' : [1 , 2 ]},
62- 'Spaces' : {'versions' : [6 ]},
63- 'Storage' : {'versions' : [6 ]},
64- 'Subnets' : {'versions' : [5 ]},
65- 'UserManager' : {'versions' : [3 ]},
66- }
67-
6826
6927def facade_versions (name , versions ):
7028 """
@@ -156,6 +114,8 @@ class Connection:
156114
157115 MAX_FRAME_SIZE = 2 ** 22
158116 "Maximum size for a single frame. Defaults to 4MB."
117+ facades : Dict [str , int ]
118+ _specified_facades : Dict [str , Sequence [int ]]
159119
160120 @classmethod
161121 async def connect (
@@ -169,7 +129,7 @@ async def connect(
169129 max_frame_size = None ,
170130 retries = 3 ,
171131 retry_backoff = 10 ,
172- specified_facades = None ,
132+ specified_facades : Optional [ Dict [ str , Dict [ Literal [ "versions" ], Sequence [ int ]]]] = None ,
173133 proxy = None ,
174134 debug_log_conn = None ,
175135 debug_log_params = {}
@@ -197,7 +157,7 @@ async def connect(
197157 :param int retry_backoff: Number of seconds to increase the wait
198158 between connection retry attempts (a backoff of 10 with 3 retries
199159 would wait 10s, 20s, and 30s).
200- :param specified_facades: Define a series of facade versions you wish to override
160+ :param specified_facades: (deprecated) define a series of facade versions you wish to override
201161 to prevent using the conservative client pinning with in the client.
202162 :param TextIOWrapper debug_log_conn: target if this is a debug log connection
203163 :param dict debug_log_params: filtering parameters for the debug-log output
@@ -247,7 +207,18 @@ async def connect(
247207 self ._retry_backoff = retry_backoff
248208
249209 self .facades = {}
250- self .specified_facades = specified_facades or {}
210+
211+ if specified_facades :
212+ warnings .warn (
213+ "The `specified_facades` argument is deprecated and will be removed soon" ,
214+ DeprecationWarning ,
215+ stacklevel = 3 ,
216+ )
217+ self ._specified_facades = {
218+ name : d ["versions" ] for name , d in specified_facades .items ()
219+ }
220+ else :
221+ self ._specified_facades = {}
251222
252223 self .messages = IdQueue ()
253224 self .monitor = Monitor (connection = self )
@@ -826,48 +797,31 @@ async def _connect_with_redirect(self, endpoints):
826797 self ._pinger_task = jasyncio .create_task (self ._pinger (), name = "Task_Pinger" )
827798
828799 # _build_facades takes the facade list that comes from the connection with the controller,
829- # validates that the client knows about them (client_facades ) and builds the facade list
830- # (into the self.specified facades) with the max versions that both the client and the controller
800+ # validates that the client knows about them (client_facade_versions ) and builds the facade list
801+ # (into the self._specified facades) with the max versions that both the client and the controller
831802 # can negotiate on
832803 def _build_facades (self , facades_from_connection ):
833804 self .facades .clear ()
834805 for facade in facades_from_connection :
835806 name = facade ['name' ]
836- # the following attempts to get the best facade version for the
837- # client. The client knows about the best facade versions it speaks,
838- # so in order to be compatible forwards and backwards we speak a
839- # common facade versions.
840- if ( name not in client_facades ) and ( name not in self . specified_facades ) :
841- # if a facade is required but the client doesn't know about
842- # it, then log a warning.
807+ if name in self . _specified_facades :
808+ client_versions = self . _specified_facades [ name ]
809+ elif name in client_facade_versions :
810+ client_versions = client_facade_versions [ name ]
811+ elif name in known_unsupported_facades :
812+ continue
813+ else :
843814 log .warning (f'unexpected facade { name } received from the controller' )
844815 continue
845816
846- try :
847- # allow the ability to specify a set of facade versions, so the
848- # client can define the non-conservative facade client pinning.
849- if name in self .specified_facades :
850- client_versions = self .specified_facades [name ]['versions' ]
851- elif name in client_facades :
852- client_versions = client_facades [name ]['versions' ]
853-
854- controller_versions = facade ['versions' ]
855- # select the max version that both the client and the controller know
856- version = max (set (client_versions ).intersection (set (controller_versions )))
857- except ValueError :
858- # this can occur if client_verisons is [1, 2] and controller_versions is [3, 4]
859- # there is just no way to know how to communicate with the facades we're trying to call.
817+ controller_versions = facade ['versions' ]
818+ candidates = set (client_versions ) & set (controller_versions )
819+ if not candidates :
860820 log .warning (f'unknown common facade version for { name } ,\n '
861821 f'versions known to client : { client_versions } \n '
862822 f'versions known to controller : { controller_versions } ' )
863- except errors .JujuConnectionError :
864- # If the facade isn't with in the local facades then it's not
865- # possible to reason about what version should be used. In this
866- # case we should log the facade was found, but we couldn't
867- # handle it.
868- log .warning (f'unexpected facade { name } found, unable to determine which version to use' )
869- else :
870- self .facades [name ] = version
823+ continue
824+ self .facades [name ] = max (candidates )
871825
872826 async def login (self ):
873827 params = {}
0 commit comments