diff --git a/api/azimuth/cluster_api/openstack.py b/api/azimuth/cluster_api/openstack.py index 61880e15..f6750f5d 100644 --- a/api/azimuth/cluster_api/openstack.py +++ b/api/azimuth/cluster_api/openstack.py @@ -30,3 +30,5 @@ def _ensure_shared_resources(self): # Just make sure that the shared tenant network exists # This allows templates to target it via a tag filter if they want self._cloud_session._tenant_network(True) + # For consistency, ensure the project share is created + self._cloud_session._project_share(True) diff --git a/api/azimuth/provider/openstack/api/__init__.py b/api/azimuth/provider/openstack/api/__init__.py index 34c667bb..95b179e6 100644 --- a/api/azimuth/provider/openstack/api/__init__.py +++ b/api/azimuth/provider/openstack/api/__init__.py @@ -1,3 +1,3 @@ from .core import Connection, ServiceNotSupported # Import the modules for each of the services -from . import block_store, coe, compute, identity, image, network, orchestration +from . import block_store, coe, compute, identity, image, network, orchestration, share diff --git a/api/azimuth/provider/openstack/api/share.py b/api/azimuth/provider/openstack/api/share.py new file mode 100644 index 00000000..b71351c1 --- /dev/null +++ b/api/azimuth/provider/openstack/api/share.py @@ -0,0 +1,89 @@ +from rackit import RootResource, EmbeddedResource, Endpoint + +from .core import ( + Service, + UnmanagedResource, + Resource, + ResourceWithDetail +) + + +class ShareType(Resource): + """ + Resource for accessing share types. + """ + class Meta: + endpoint = '/types' + resource_key = "share_type" + resource_list_key = "share_types" + + +class Share(ResourceWithDetail): + """ + Resource for accessing shares. + """ + class Meta: + endpoint = '/shares' + + def grant_rw_access(self, username): + self._action('action', { + 'allow_access': { + "access_level": "rw", + "access_type": "cephx", + "access_to": username, + } + }) + + +class ShareAccess(Resource): + """ + Resource for share access lists. + """ + class Meta: + endpoint = '/share-access-rules' + resource_key = "access" + resource_list_key = "access_list" + + +class AbsoluteLimits(UnmanagedResource): + """ + Represents the absolute limits for a project. + """ + class Meta: + aliases = dict( + # TODO(johngarbutt): fill out the rest? + total_shares_gb='maxTotalShareGigabytes', + total_shares_gb_used='totalShareGigabytesUsed', + total_shares='maxTotalShares', + total_shares_used='totalSharesUsed', + ) + + +class ShareLimits(UnmanagedResource): + """ + Represents the limits for a project. + + This is not a REST-ful resource, so is unmanaged. + """ + class Meta: + endpoint = "/limits" + + absolute = EmbeddedResource(AbsoluteLimits) + + +class ShareService(Service): + """ + OpenStack service class for the compute service. + """ + name = "share" + catalog_type = "sharev2" + path_prefix = '/v2/{project_id}' + + limits = Endpoint(ShareLimits) + shares = RootResource(Share) + types = RootResource(ShareType) + access = RootResource(ShareAccess) + + def prepare_request(self, request): + request.headers["X-OpenStack-Manila-API-Version"] = "2.51" + return super().prepare_request(request) diff --git a/api/azimuth/provider/openstack/provider.py b/api/azimuth/provider/openstack/provider.py index 4c3bd3fa..407f9a33 100644 --- a/api/azimuth/provider/openstack/provider.py +++ b/api/azimuth/provider/openstack/provider.py @@ -10,6 +10,7 @@ import logging import random import re +import time import dateutil.parser @@ -140,6 +141,8 @@ class Provider(base.Provider): fragment ``{tenant_name}``. create_internal_net: If ``True`` (the default), then the internal network is auto-created when a tagged network or templated network cannot be found. + manila_project_share_gb: If >0 (the default is 0), then + manila project share is auto created with specified size. internal_net_cidr: The CIDR for the internal network when it is auto-created (default ``192.168.3.0/24``). internal_net_dns_nameservers: The DNS nameservers for the internal network when it is @@ -163,6 +166,7 @@ def __init__(self, auth_url, internal_net_template = None, external_net_template = None, create_internal_net = True, + manila_project_share_gb = 0, internal_net_cidr = "192.168.3.0/24", internal_net_dns_nameservers = None, az_backdoor_net_map = None, @@ -176,6 +180,9 @@ def __init__(self, auth_url, self._internal_net_template = internal_net_template self._external_net_template = external_net_template self._create_internal_net = create_internal_net + self._manila_project_share_gb = 0 + if manila_project_share_gb: + self._manila_project_share_gb = int(manila_project_share_gb) self._internal_net_cidr = internal_net_cidr self._internal_net_dns_nameservers = internal_net_dns_nameservers self._az_backdoor_net_map = az_backdoor_net_map or dict() @@ -202,6 +209,7 @@ def from_token(self, token): internal_net_template = self._internal_net_template, external_net_template = self._external_net_template, create_internal_net = self._create_internal_net, + manila_project_share_gb = self._manila_project_share_gb, internal_net_cidr = self._internal_net_cidr, internal_net_dns_nameservers = self._internal_net_dns_nameservers, az_backdoor_net_map = self._az_backdoor_net_map, @@ -220,6 +228,7 @@ def __init__(self, connection, internal_net_template = None, external_net_template = None, create_internal_net = True, + manila_project_share_gb = 0, internal_net_cidr = "192.168.3.0/24", internal_net_dns_nameservers = None, az_backdoor_net_map = None, @@ -229,6 +238,7 @@ def __init__(self, connection, self._internal_net_template = internal_net_template self._external_net_template = external_net_template self._create_internal_net = create_internal_net + self._manila_project_share_gb = manila_project_share_gb self._internal_net_cidr = internal_net_cidr self._internal_net_dns_nameservers = internal_net_dns_nameservers self._az_backdoor_net_map = az_backdoor_net_map or dict() @@ -366,6 +376,7 @@ def scoped_session(self, tenancy): internal_net_template = self._internal_net_template, external_net_template = self._external_net_template, create_internal_net = self._create_internal_net, + manila_project_share_gb = self._manila_project_share_gb, internal_net_cidr = self._internal_net_cidr, internal_net_dns_nameservers = self._internal_net_dns_nameservers, az_backdoor_net_map = self._az_backdoor_net_map, @@ -397,6 +408,7 @@ def __init__(self, username, internal_net_template = None, external_net_template = None, create_internal_net = True, + manila_project_share_gb = 0, internal_net_cidr = "192.168.3.0/24", internal_net_dns_nameservers = None, az_backdoor_net_map = None, @@ -408,11 +420,21 @@ def __init__(self, username, self._internal_net_template = internal_net_template self._external_net_template = external_net_template self._create_internal_net = create_internal_net + self._manila_project_share_gb = manila_project_share_gb self._internal_net_cidr = internal_net_cidr self._internal_net_dns_nameservers = internal_net_dns_nameservers self._az_backdoor_net_map = az_backdoor_net_map or dict() self._backdoor_vnic_type = backdoor_vnic_type + # TODO(johngarbutt): consider moving some of this to config + # and/or hopefully having this feature on by default + # and auto detecting when its available, which is not currently + # feasible. + self._project_share_name = "azimuth-project-share" + prefix = "proj" + project_id_safe = self._connection.project_id.replace("-", "") + self._project_share_user = prefix + project_id_safe + def _log(self, message, *args, level = logging.INFO, **kwargs): logger.log( level, @@ -696,6 +718,101 @@ def _tenant_network(self, create_network = False): else: raise errors.InvalidOperationError("Could not find internal network.") + def _project_share(self, create_share=True): + """ + Returns the project specific Manila share. + + If we are not configured to create the project share, + we do nothing here, and return None. + + If we are configured to create the project share, + we look to see if a valid share is already created. + If we find a valid share, we return that object. + + Finally, we look to create the share dynamically, + then return that share. + + If this project has not available share type in + Manila, we simply log that we can't create a share + for this project, and return None. + """ + if not self._manila_project_share_gb: + return + + # find if project share exists + project_share = None + current_shares = self._connection.share.shares.all() + for share in current_shares: + if share.name == self._project_share_name: + project_share = share + if project_share: + # double check share has the correct protocol and is available + share_details = self._connection.share.shares.get(project_share.id) + self._log(f"Got share details f{share_details}") + if share_details.share_proto.upper() != "CEPHFS": + raise errors.ImproperlyConfiguredError( + "Currently only support CephFS shares!") + if share_details.status.lower() != "available": + raise errors.ImproperlyConfiguredError( + "Project share is not available!") + if share_details.access_rules_status.lower() != "active": + raise errors.ImproperlyConfiguredError( + "Project share has a problem with its access rules!") + + access_list = list(self._connection.share.access.all( + share_id=project_share.id)) + found_expected_access = False + for access in access_list: + if access.access_to == self._project_share_user: + found_expected_access = True + break + if not found_expected_access: + raise errors.ImproperlyConfiguredError("can't find the expected access rule!") + self._log(f"Found project share for: {self._connection.project_id}") + + # no share found, create if required + if not project_share and create_share: + self._log(f"Creating project share for: {self._connection.project_id}") + + # Find share type + default_share_type = None + all_types = list(self._connection.share.types.all()) + if len(all_types) == 1: + default_share_type = all_types[0] + else: + for share_type in all_types: + if share_type.is_default: + default_share_type = share_type + break + if not default_share_type: + # Silent ignore here, as it usually means project + # has not been setup for manila + self._log("Unable to find valid share type!") + return + + # TODO(johngarbutt) need to support non-ceph types eventually + project_share = self._connection.share.shares.create( + share_proto="CephFS", + size=self._manila_project_share_gb, + name=self._project_share_name, + description="Project share auto-created by Azimuth.", + share_type=default_share_type.id) + + # wait for share to be available before trying to grant access + for _ in range(10): + latest = self._connection.share.shares.get(project_share.id) + if latest.status.lower() == "available": + break + if latest.status.lower() == "error": + raise errors.Error("Unable to create project share.") + time.sleep(0.1) + + project_share.grant_rw_access(self._project_share_user) + # TODO(johngarbutt) should we wait for access to be granted? + self._log(f"Created new project share: {project_share.id}") + + return project_share + def _external_network(self): """ Returns the external network that connects the tenant router to the outside world. @@ -1389,7 +1506,7 @@ def cluster_parameters(self): """ # Inject information about the networks to use external_network = self._external_network().name - return dict( + params = dict( # Legacy name cluster_floating_network = external_network, # New name @@ -1397,6 +1514,18 @@ def cluster_parameters(self): cluster_network = self._tenant_network(True).name ) + # If configured to, find if we can have a project share + project_share = self._project_share(True) + if project_share: + params["cluster_project_manila_share"] = True + params["cluster_project_manila_share_name"] = project_share.name + user = self._project_share_user + params["cluster_project_manila_share_user"] = user + else: + params["cluster_project_manila_share"] = False + + return params + def cluster_modify(self, cluster): """ See :py:meth:`.base.ScopedSession.cluster_modify`. diff --git a/chart/files/api/settings/04-cloud-provider.yaml b/chart/files/api/settings/04-cloud-provider.yaml index 0c147dc4..80ecd659 100644 --- a/chart/files/api/settings/04-cloud-provider.yaml +++ b/chart/files/api/settings/04-cloud-provider.yaml @@ -16,6 +16,7 @@ AZIMUTH: EXTERNAL_NET_TEMPLATE: {{ . | quote }} {{- end }} CREATE_INTERNAL_NET: {{ .Values.provider.openstack.createInternalNet }} + MANILA_PROJECT_SHARE_GB: {{ .Values.provider.openstack.manilaProjectShareGB }} {{- with .Values.provider.openstack.internalNetCidr }} INTERNAL_NET_CIDR: {{ . | quote }} {{- end }} diff --git a/chart/values.yaml b/chart/values.yaml index 8dbaa4d6..22b52064 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -267,6 +267,8 @@ provider: externalNetTemplate: # Indicates whether tenant internal networks should be auto-created if not present createInternalNet: true + # If larger than zero, project specific manila share should be auto-created + manilaProjectShareGB: 0 # The CIDR to use for auto-created tenant internal networks # Defaults to 192.168.3.0/24 if not given, which should be OK for most circumstances internalNetCidr: