39
39
40
40
_SSH_TIMEOUT = 30
41
41
_TEST_STRING = "test_string"
42
+ # Max nova compute we support is 2.91, because
43
+ # - 2.96 has a bug with server list https://bugs.launchpad.net/nova/+bug/2095364
44
+ # - 2.92 requires public key to be set in the keypair, which is not supported by the app
45
+ # https://docs.openstack.org/api-ref/compute/#import-or-create-keypair
46
+ _MAX_NOVA_COMPUTE_API_VERSION = "2.91"
42
47
43
48
SecurityRuleDict = dict [str , Any ]
44
49
@@ -153,33 +158,6 @@ def exception_handling_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
153
158
return exception_handling_wrapper
154
159
155
160
156
- @contextmanager
157
- def _get_openstack_connection (credentials : OpenStackCredentials ) -> Iterator [OpenstackConnection ]:
158
- """Create a connection context managed object, to be used within with statements.
159
-
160
- Using the context manager ensures that the connection is properly closed after use.
161
-
162
- Args:
163
- credentials: The OpenStack authorization information.
164
-
165
- Yields:
166
- An openstack.connection.Connection object.
167
- """
168
- # api documents that keystoneauth1.exceptions.MissingRequiredOptions can be raised but
169
- # I could not reproduce it. Therefore, no catch here for such exception.
170
- with openstack .connect (
171
- auth_url = credentials .auth_url ,
172
- project_name = credentials .project_name ,
173
- username = credentials .username ,
174
- password = credentials .password ,
175
- region_name = credentials .region_name ,
176
- user_domain_name = credentials .user_domain_name ,
177
- project_domain_name = credentials .project_domain_name ,
178
- ) as conn :
179
- conn .authorize ()
180
- yield conn
181
-
182
-
183
161
class OpenstackCloud :
184
162
"""Client to interact with OpenStack cloud.
185
163
@@ -237,7 +215,7 @@ def launch_instance(
237
215
instance_id = runner_identity .instance_id
238
216
metadata = runner_identity .metadata
239
217
240
- with _get_openstack_connection ( credentials = self ._credentials ) as conn :
218
+ with self ._get_openstack_connection ( ) as conn :
241
219
security_group = OpenstackCloud ._ensure_security_group (conn , ingress_tcp_ports )
242
220
keypair = self ._setup_keypair (conn , runner_identity .instance_id )
243
221
meta = metadata .as_dict ()
@@ -283,7 +261,7 @@ def get_instance(self, instance_id: InstanceID) -> OpenstackInstance | None:
283
261
"""
284
262
logger .info ("Getting openstack server with %s" , instance_id )
285
263
286
- with _get_openstack_connection ( credentials = self ._credentials ) as conn :
264
+ with self ._get_openstack_connection ( ) as conn :
287
265
server : OpenstackServer = conn .get_server (name_or_id = instance_id .name )
288
266
if server is not None :
289
267
return OpenstackInstance (server , self .prefix )
@@ -298,7 +276,7 @@ def delete_instance(self, instance_id: InstanceID) -> None:
298
276
"""
299
277
logger .info ("Deleting openstack server with %s" , instance_id )
300
278
301
- with _get_openstack_connection ( credentials = self ._credentials ) as conn :
279
+ with self ._get_openstack_connection ( ) as conn :
302
280
self ._delete_instance (conn , instance_id )
303
281
304
282
def _delete_instance (self , conn : OpenstackConnection , instance_id : InstanceID ) -> None :
@@ -400,7 +378,7 @@ def get_instances(self) -> tuple[OpenstackInstance, ...]:
400
378
"""
401
379
logger .info ("Getting all openstack servers managed by the charm" )
402
380
403
- with _get_openstack_connection ( credentials = self ._credentials ) as conn :
381
+ with self ._get_openstack_connection ( ) as conn :
404
382
instance_list = list (self ._get_openstack_instances (conn ))
405
383
server_names = set (server .name for server in instance_list )
406
384
@@ -417,7 +395,7 @@ def get_instances(self) -> tuple[OpenstackInstance, ...]:
417
395
@_catch_openstack_errors
418
396
def cleanup (self ) -> None :
419
397
"""Cleanup unused key files and openstack keypairs."""
420
- with _get_openstack_connection ( credentials = self ._credentials ) as conn :
398
+ with self ._get_openstack_connection ( ) as conn :
421
399
instances = self ._get_openstack_instances (conn )
422
400
exclude_keyfiles_set = {
423
401
self ._get_key_path (InstanceID .build_from_name (self .prefix , server .name ))
@@ -650,6 +628,86 @@ def _ensure_security_group(
650
628
)
651
629
return security_group
652
630
631
+ @contextmanager
632
+ def _get_openstack_connection (self ) -> Iterator [OpenstackConnection ]:
633
+ """Create a connection context managed object, to be used within with statements.
634
+
635
+ Using the context manager ensures that the connection is properly closed after use.
636
+
637
+ Yields:
638
+ An openstack.connection.Connection object.
639
+ """
640
+ # api documents that keystoneauth1.exceptions.MissingRequiredOptions can be raised but
641
+ # I could not reproduce it. Therefore, no catch here for such exception.
642
+
643
+ with openstack .connect (
644
+ auth_url = self ._credentials .auth_url ,
645
+ project_name = self ._credentials .project_name ,
646
+ username = self ._credentials .username ,
647
+ password = self ._credentials .password ,
648
+ region_name = self ._credentials .region_name ,
649
+ user_domain_name = self ._credentials .user_domain_name ,
650
+ project_domain_name = self ._credentials .project_domain_name ,
651
+ compute_api_version = self ._max_compute_api_version ,
652
+ ) as conn :
653
+ conn .authorize ()
654
+ yield conn
655
+
656
+ @functools .cached_property
657
+ def _max_compute_api_version (self ) -> str :
658
+ """Determine the maximum compute API version supported by the client.
659
+
660
+ The sdk does not support versions greater than 2.95, so we need to ensure that the
661
+ maximum version returned by the OpenStack cloud is not greater than that.
662
+ https://bugs.launchpad.net/nova/+bug/2095364
663
+
664
+ Returns:
665
+ The maximum compute API version to use for the client.
666
+ """
667
+ max_version = self ._determine_max_compute_api_version_by_cloud ()
668
+ if self ._version_greater_than (max_version , _MAX_NOVA_COMPUTE_API_VERSION ):
669
+ logger .warning (
670
+ "The maximum compute API version %s is greater than the supported version %s. "
671
+ "Using the maximum supported version." ,
672
+ max_version ,
673
+ _MAX_NOVA_COMPUTE_API_VERSION ,
674
+ )
675
+ return _MAX_NOVA_COMPUTE_API_VERSION
676
+ return max_version
677
+
678
+ def _determine_max_compute_api_version_by_cloud (self ) -> str :
679
+ """Determine the maximum compute API version supported by the OpenStack cloud.
680
+
681
+ Returns:
682
+ The maximum compute API version as a string.
683
+ """
684
+ with openstack .connect (
685
+ auth_url = self ._credentials .auth_url ,
686
+ project_name = self ._credentials .project_name ,
687
+ username = self ._credentials .username ,
688
+ password = self ._credentials .password ,
689
+ region_name = self ._credentials .region_name ,
690
+ user_domain_name = self ._credentials .user_domain_name ,
691
+ project_domain_name = self ._credentials .project_domain_name ,
692
+ ) as conn :
693
+ version_endpoint = conn .compute .get_endpoint ()
694
+ resp = conn .session .get (version_endpoint )
695
+ return resp .json ()["version" ]["version" ]
696
+
697
+ def _version_greater_than (self , version1 : str , version2 : str ) -> bool :
698
+ """Compare two OpenStack API versions.
699
+
700
+ Args:
701
+ version1: The first version to compare.
702
+ version2: The second version to compare.
703
+
704
+ Returns:
705
+ True if version1 is greater than version2, False otherwise.
706
+ """
707
+ return tuple (int (x ) for x in version1 .split ("." )) > tuple (
708
+ int (x ) for x in version2 .split ("." )
709
+ )
710
+
653
711
654
712
def get_missing_security_rules (
655
713
security_group : OpenstackSecurityGroup , ingress_tcp_ports : list [int ] | None
0 commit comments