start the services.
"""
KNOWN_SERVICE_TYPES = 'alertmanager crash grafana iscsi loki promtail mds mgr mon nfs ' \
- 'node-exporter osd prometheus rbd-mirror rgw agent ' \
- 'container ingress cephfs-mirror snmp-gateway ceph-exporter'.split()
+ 'node-exporter osd prometheus rbd-mirror rgw agent ceph-exporter ' \
+ 'container ingress cephfs-mirror snmp-gateway jaeger-tracing ' \
+ 'elasticsearch jaeger-agent jaeger-collector jaeger-query'.split()
REQUIRES_SERVICE_ID = 'iscsi mds nfs rgw container ingress '.split()
MANAGED_CONFIG_OPTIONS = [
'mds_join_fs',
from ceph.deployment.drive_group import DriveGroupSpec
ret = {
+ 'mon': MONSpec,
'rgw': RGWSpec,
'nfs': NFSServiceSpec,
'osd': DriveGroupSpec,
'loki': MonitoringSpec,
'promtail': MonitoringSpec,
'snmp-gateway': SNMPGatewaySpec,
+ 'elasticsearch': TracingSpec,
+ 'jaeger-agent': TracingSpec,
+ 'jaeger-collector': TracingSpec,
+ 'jaeger-query': TracingSpec,
+ 'jaeger-tracing': TracingSpec,
}.get(service_type, cls)
if ret == ServiceSpec and not service_type:
raise SpecValidationError('Spec needs a "service_type" key.')
understanding of what fields are special for a give service type.
Note, we'll need to stay compatible with both versions for the
- the next two major releases (octoups, pacific).
+ the next two major releases (octopus, pacific).
:param json_spec: A valid dict with ServiceSpec
config: Optional[Dict[str, str]] = None,
networks: Optional[List[str]] = None,
port: Optional[int] = None,
+ virtual_ip: Optional[str] = None,
extra_container_args: Optional[List[str]] = None,
extra_entrypoint_args: Optional[List[str]] = None,
custom_configs: Optional[List[CustomConfig]] = None,
extra_entrypoint_args=extra_entrypoint_args, custom_configs=custom_configs)
self.port = port
+ self.virtual_ip = virtual_ip
def get_port_start(self) -> List[int]:
if self.port:
service_id: myrealm.myzone
spec:
rgw_realm: myrealm
+ rgw_zonegroup: myzonegroup
rgw_zone: myzone
ssl: true
rgw_frontend_port: 1234
MANAGED_CONFIG_OPTIONS = ServiceSpec.MANAGED_CONFIG_OPTIONS + [
'rgw_zone',
'rgw_realm',
+ 'rgw_zonegroup',
'rgw_frontends',
]
service_id: Optional[str] = None,
placement: Optional[PlacementSpec] = None,
rgw_realm: Optional[str] = None,
+ rgw_zonegroup: Optional[str] = None,
rgw_zone: Optional[str] = None,
rgw_frontend_port: Optional[int] = None,
rgw_frontend_ssl_certificate: Optional[List[str]] = None,
rgw_frontend_type: Optional[str] = None,
+ rgw_frontend_extra_args: Optional[List[str]] = None,
unmanaged: bool = False,
ssl: bool = False,
preview_only: bool = False,
extra_container_args: Optional[List[str]] = None,
extra_entrypoint_args: Optional[List[str]] = None,
custom_configs: Optional[List[CustomConfig]] = None,
+ rgw_realm_token: Optional[str] = None,
+ update_endpoints: Optional[bool] = False,
+ zone_endpoints: Optional[str] = None # commad separated endpoints list
):
assert service_type == 'rgw', service_type
custom_configs=custom_configs)
#: The RGW realm associated with this service. Needs to be manually created
+ #: if the spec is being applied directly to cephdam. In case of rgw module
+ #: the realm is created automatically.
self.rgw_realm: Optional[str] = rgw_realm
+ #: The RGW zonegroup associated with this service. Needs to be manually created
+ #: if the spec is being applied directly to cephdam. In case of rgw module
+ #: the zonegroup is created automatically.
+ self.rgw_zonegroup: Optional[str] = rgw_zonegroup
#: The RGW zone associated with this service. Needs to be manually created
+ #: if the spec is being applied directly to cephdam. In case of rgw module
+ #: the zone is created automatically.
self.rgw_zone: Optional[str] = rgw_zone
#: Port of the RGW daemons
self.rgw_frontend_port: Optional[int] = rgw_frontend_port
self.rgw_frontend_ssl_certificate: Optional[List[str]] = rgw_frontend_ssl_certificate
#: civetweb or beast (default: beast). See :ref:`rgw_frontends`
self.rgw_frontend_type: Optional[str] = rgw_frontend_type
+ #: List of extra arguments for rgw_frontend in the form opt=value. See :ref:`rgw_frontends`
+ self.rgw_frontend_extra_args: Optional[List[str]] = rgw_frontend_extra_args
#: enable SSL
self.ssl = ssl
+ self.rgw_realm_token = rgw_realm_token
+ self.update_endpoints = update_endpoints
+ self.zone_endpoints = zone_endpoints
def get_port_start(self) -> List[int]:
return [self.get_port()]
raise SpecValidationError(
'Cannot add RGW: Realm specified but no zone specified')
if self.rgw_zone and not self.rgw_realm:
- raise SpecValidationError(
- 'Cannot add RGW: Zone specified but no realm specified')
+ raise SpecValidationError('Cannot add RGW: Zone specified but no realm specified')
+
+ if self.rgw_frontend_type is not None:
+ if self.rgw_frontend_type not in ['beast', 'civetweb']:
+ raise SpecValidationError(
+ 'Invalid rgw_frontend_type value. Valid values are: beast, civetweb.\n'
+ 'Additional rgw type parameters can be passed using rgw_frontend_extra_args.'
+ )
yaml.add_representer(RGWSpec, ServiceSpec.yaml_representer)
virtual_interface_networks: Optional[List[str]] = [],
unmanaged: bool = False,
ssl: bool = False,
+ keepalive_only: bool = False,
extra_container_args: Optional[List[str]] = None,
extra_entrypoint_args: Optional[List[str]] = None,
custom_configs: Optional[List[CustomConfig]] = None,
self.virtual_interface_networks = virtual_interface_networks or []
self.unmanaged = unmanaged
self.ssl = ssl
+ self.keepalive_only = keepalive_only
def get_port_start(self) -> List[int]:
- return [cast(int, self.frontend_port),
- cast(int, self.monitor_port)]
+ ports = []
+ if self.frontend_port is not None:
+ ports.append(cast(int, self.frontend_port))
+ if self.monitor_port is not None:
+ ports.append(cast(int, self.monitor_port))
+ return ports
def get_virtual_ip(self) -> Optional[str]:
return self.virtual_ip
if not self.backend_service:
raise SpecValidationError(
'Cannot add ingress: No backend_service specified')
- if not self.frontend_port:
+ if not self.keepalive_only and not self.frontend_port:
raise SpecValidationError(
'Cannot add ingress: No frontend_port specified')
if not self.monitor_port:
config: Optional[Dict[str, str]] = None,
networks: Optional[List[str]] = None,
port: Optional[int] = None,
+ protocol: Optional[str] = 'https',
initial_admin_password: Optional[str] = None,
+ anonymous_access: Optional[bool] = True,
extra_container_args: Optional[List[str]] = None,
extra_entrypoint_args: Optional[List[str]] = None,
custom_configs: Optional[List[CustomConfig]] = None,
custom_configs=custom_configs)
self.initial_admin_password = initial_admin_password
+ self.anonymous_access = anonymous_access
+ self.protocol = protocol
+
+ def validate(self) -> None:
+ super(GrafanaSpec, self).validate()
+ if self.protocol not in ['http', 'https']:
+ err_msg = f"Invalid protocol '{self.protocol}'. Valid values are: 'http', 'https'."
+ raise SpecValidationError(err_msg)
+
+ if not self.anonymous_access and not self.initial_admin_password:
+ err_msg = ('Either initial_admin_password must be set or anonymous_access '
+ 'must be set to true. Otherwise the grafana dashboard will '
+ 'be inaccessible.')
+ raise SpecValidationError(err_msg)
yaml.add_representer(GrafanaSpec, ServiceSpec.yaml_representer)
self.retention_time = retention_time.strip() if retention_time else None
self.retention_size = retention_size.strip() if retention_size else None
+ def validate(self) -> None:
+ super(PrometheusSpec, self).validate()
+
+ if self.retention_time:
+ valid_units = ['y', 'w', 'd', 'h', 'm', 's']
+ m = re.search(rf"^(\d+)({'|'.join(valid_units)})$", self.retention_time)
+ if not m:
+ units = ', '.join(valid_units)
+ raise SpecValidationError(f"Invalid retention time. Valid units are: {units}")
+ if self.retention_size:
+ valid_units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']
+ m = re.search(rf"^(\d+)({'|'.join(valid_units)})$", self.retention_size)
+ if not m:
+ units = ', '.join(valid_units)
+ raise SpecValidationError(f"Invalid retention size. Valid units are: {units}")
+
yaml.add_representer(PrometheusSpec, ServiceSpec.yaml_representer)
yaml.add_representer(MDSSpec, ServiceSpec.yaml_representer)
+class MONSpec(ServiceSpec):
+ def __init__(self,
+ service_type: str,
+ service_id: Optional[str] = None,
+ placement: Optional[PlacementSpec] = None,
+ count: Optional[int] = None,
+ config: Optional[Dict[str, str]] = None,
+ unmanaged: bool = False,
+ preview_only: bool = False,
+ networks: Optional[List[str]] = None,
+ extra_container_args: Optional[List[str]] = None,
+ custom_configs: Optional[List[CustomConfig]] = None,
+ crush_locations: Optional[Dict[str, List[str]]] = None,
+ ):
+ assert service_type == 'mon'
+ super(MONSpec, self).__init__('mon', service_id=service_id,
+ placement=placement,
+ count=count,
+ config=config,
+ unmanaged=unmanaged,
+ preview_only=preview_only,
+ networks=networks,
+ extra_container_args=extra_container_args,
+ custom_configs=custom_configs)
+
+ self.crush_locations = crush_locations
+ self.validate()
+
+ def validate(self) -> None:
+ if self.crush_locations:
+ for host, crush_locs in self.crush_locations.items():
+ try:
+ assert_valid_host(host)
+ except SpecValidationError as e:
+ err_str = f'Invalid hostname found in spec crush locations: {e}'
+ raise SpecValidationError(err_str)
+ for cloc in crush_locs:
+ if '=' not in cloc or len(cloc.split('=')) != 2:
+ err_str = ('Crush locations must be of form <bucket>=<location>. '
+ f'Found crush location: {cloc}')
+ raise SpecValidationError(err_str)
+
+
+yaml.add_representer(MONSpec, ServiceSpec.yaml_representer)
+
+
+class TracingSpec(ServiceSpec):
+ SERVICE_TYPES = ['elasticsearch', 'jaeger-collector', 'jaeger-query', 'jaeger-agent']
+
+ def __init__(self,
+ service_type: str,
+ es_nodes: Optional[str] = None,
+ without_query: bool = False,
+ service_id: Optional[str] = None,
+ config: Optional[Dict[str, str]] = None,
+ networks: Optional[List[str]] = None,
+ placement: Optional[PlacementSpec] = None,
+ unmanaged: bool = False,
+ preview_only: bool = False
+ ):
+ assert service_type in TracingSpec.SERVICE_TYPES + ['jaeger-tracing']
+
+ super(TracingSpec, self).__init__(
+ service_type, service_id,
+ placement=placement, unmanaged=unmanaged,
+ preview_only=preview_only, config=config,
+ networks=networks)
+ self.without_query = without_query
+ self.es_nodes = es_nodes
+
+ def get_port_start(self) -> List[int]:
+ return [self.get_port()]
+
+ def get_port(self) -> int:
+ return {'elasticsearch': 9200,
+ 'jaeger-agent': 6799,
+ 'jaeger-collector': 14250,
+ 'jaeger-query': 16686}[self.service_type]
+
+ def get_tracing_specs(self) -> List[ServiceSpec]:
+ assert self.service_type == 'jaeger-tracing'
+ specs: List[ServiceSpec] = []
+ daemons: Dict[str, Optional[PlacementSpec]] = {
+ daemon: None for daemon in TracingSpec.SERVICE_TYPES}
+
+ if self.es_nodes:
+ del daemons['elasticsearch']
+ if self.without_query:
+ del daemons['jaeger-query']
+ if self.placement:
+ daemons.update({'jaeger-collector': self.placement})
+
+ for daemon, daemon_placement in daemons.items():
+ specs.append(TracingSpec(service_type=daemon,
+ es_nodes=self.es_nodes,
+ placement=daemon_placement,
+ unmanaged=self.unmanaged,
+ config=self.config,
+ networks=self.networks,
+ preview_only=self.preview_only
+ ))
+ return specs
+
+
+yaml.add_representer(TracingSpec, ServiceSpec.yaml_representer)
+
+
class TunedProfileSpec():
def __init__(self,
profile_name: str,