11# Copyright 2017 LasLabs Inc.
2+ # Copyright 2023-2024 Therp BV.
23# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
34
45from contextlib import contextmanager
@@ -12,42 +13,47 @@ class ExternalSystem(models.Model):
1213 _description = "External System"
1314
1415 name = fields .Char (
15- required = True ,
16- help = "This is the canonical (humanized) name for the system." ,
16+ required = True , help = "This is the canonical (humanized) name for the system."
17+ )
18+ scheme = fields .Char (
19+ help = "This is the protocol used to communicate with the system."
1720 )
1821 host = fields .Char (
19- help = "This is the domain or IP address that the system can be reached " " at.",
22+ help = "This is the domain or IP address that the system can be reached at."
2023 )
2124 port = fields .Integer (
22- help = "This is the port number that the system is listening on." ,
25+ help = "This is the port number that the system is listening on."
2326 )
2427 username = fields .Char (
25- help = "This is the username that is used for authenticating to this "
26- "system, if applicable." ,
28+ help = "This is the username that is used for authenticating to this"
29+ " system, if applicable."
2730 )
2831 password = fields .Char (
29- help = "This is the password that is used for authenticating to this "
30- "system, if applicable." ,
32+ help = "This is the password that is used for authenticating to this"
33+ " system, if applicable."
3134 )
3235 private_key = fields .Text (
33- help = "This is the private key that is used for authenticating to "
34- "this system, if applicable." ,
36+ help = "This is the private key that is used for authenticating to"
37+ " this system, if applicable."
3538 )
3639 private_key_password = fields .Text (
37- help = "This is the password to unlock the private key that was "
38- "provided for this sytem." ,
40+ help = "This is the password to unlock the private key that was"
41+ " provided for this sytem."
42+ )
43+ private_key_thumbprint = fields .Text (
44+ help = "The thumbprint generated by AAD when you upload your public cert"
3945 )
4046 fingerprint = fields .Text (
41- help = "This is the fingerprint that is advertised by this system in "
42- "order to validate its identity." ,
47+ help = "This is the fingerprint that is advertised by this system in"
48+ " order to validate its identity."
4349 )
4450 ignore_fingerprint = fields .Boolean (
4551 default = True ,
46- help = "Set this to `True` in order to ignore an invalid/unknown "
47- "fingerprint from the system." ,
52+ help = "Set this to `True` in order to ignore an invalid/unknown"
53+ " fingerprint from the system." ,
4854 )
4955 remote_path = fields .Char (
50- help = "Restrict to this directory path on the remote, if applicable." ,
56+ help = "Restrict to this directory path on the remote, if applicable."
5157 )
5258 company_ids = fields .Many2many (
5359 string = "Companies" ,
@@ -57,25 +63,29 @@ class ExternalSystem(models.Model):
5763 help = "Access to this system is restricted to these companies." ,
5864 )
5965 system_type = fields .Selection (
60- selection = "_get_system_types" ,
66+ # Use lambda selection, otherwise subclasses loaded later will not be found.
67+ selection = lambda self : self ._get_system_types (),
6168 required = True ,
6269 )
63- interface = fields .Reference (
64- selection = "_get_system_types" ,
65- readonly = True ,
66- help = "This is the interface that this system represents. It is "
67- "created automatically upon creation of the external system." ,
68- )
6970
7071 _sql_constraints = [
71- ("name_uniq" , "UNIQUE(name)" , "Connection name must be unique." ),
72+ ("name_uniq" , "UNIQUE(name)" , "Connection name must be unique." )
7273 ]
7374
7475 @api .model
7576 def _get_system_types (self ):
7677 """Return the adapter interface models that are installed."""
7778 adapter = self .env ["external.system.adapter" ]
78- return [(m , self .env [m ]._description ) for m in adapter ._inherit_children ]
79+ subclasses = set ()
80+ work = [adapter ]
81+ while work :
82+ parent = work .pop ()
83+ for child in parent ._inherit_children :
84+ subclass = self .env [child ]
85+ if subclass not in subclasses :
86+ subclasses .add (subclass )
87+ work .append (subclass )
88+ return [(m ._name , m ._description ) for m in subclasses ]
7989
8090 @api .constrains ("fingerprint" , "ignore_fingerprint" )
8191 def check_fingerprint_ignore_fingerprint (self ):
@@ -84,8 +94,8 @@ def check_fingerprint_ignore_fingerprint(self):
8494 if not record .ignore_fingerprint and not record .fingerprint :
8595 raise ValidationError (
8696 _ (
87- "Fingerprint cannot be empty when Ignore Fingerprint is "
88- "not checked." ,
97+ "Fingerprint cannot be empty when Ignore Fingerprint is"
98+ " not checked."
8999 )
90100 )
91101
@@ -100,24 +110,55 @@ def client(self):
100110 mixed: An object representing the client connection to the remote
101111 system.
102112 """
103- with self .interface .client () as client :
113+ self .ensure_one ()
114+ adapter = None
115+ client = None
116+ try :
117+ adapter = self ._get_adapter ()
118+ client = adapter .external_get_client ()
104119 yield client
105-
106- @api .model_create_multi
107- def create (self , vals_list ):
108- """Create the interface for the record and assign to ``interface``."""
109- records = self .browse ([])
110- for vals in vals_list :
111- record = super ().create (vals )
112- if not self .env .context .get ("no_create_interface" ):
113- interface = self .env [vals ["system_type" ]].create (
114- {"system_id" : record .id }
115- )
116- record .interface = interface
117- records += record
118- return records
120+ finally :
121+ if client :
122+ adapter .external_destroy_client (client )
119123
120124 def action_test_connection (self ):
121- """Test the connection to the external system."""
125+ """Test the connection to the external system.
126+
127+ Any unexpected exception will be transformed into a
128+ ValidationError. A ValidationError will also be raised
129+ if no client is returned.
130+ """
122131 self .ensure_one ()
123- self .interface .external_test_connection ()
132+ try :
133+ with self .client () as client :
134+ if client is None :
135+ raise ValidationError (
136+ _ ("Client connection failed for system %s" ) % self .name
137+ )
138+ return True
139+ except Exception as exc :
140+ raise ValidationError (
141+ _ (
142+ "Unexpected error %(exception)s"
143+ " when connecting to %(system_name)s"
144+ )
145+ % {
146+ "exception" : exc ,
147+ "system_name" : self .name ,
148+ }
149+ ) from None
150+
151+ def _get_adapter (self ):
152+ """Trivial method to get adapter from system type.
153+
154+ Adding the system to the context allows the AbstractModel for the adapter,
155+ that can not be instantiated, to still hold information, through a class
156+ property, that can be accessed by all methods in derived classes.
157+
158+ An alternative would be to use standard python classes, but that would take
159+ away the possibility to extend them in the Odoo way.
160+
161+ To even further extend the possibility of the adapter to have its own
162+ runtime memory, we add an (at first) empty dictionary to the context.
163+ """
164+ return self .with_context (system = self , adapter_memory = {}).env [self .system_type ]
0 commit comments