text: """Agent and Agent group definitions and settings dataclasses."""
  import dataclasses
  import io
  import re
  import logging
  from typing import List, Optional
  
  import docker
  
  from ostorlab.agent.schema import loader
  from ostorlab.runtimes.proto import agent_instance_settings_pb2
  from ostorlab.utils import defintions, version
  
  logger = logging.getLogger(__name__)
  
  @dataclasses.dataclass
  class AgentSettings:
      """Agent instance lists the settings of running instance of an agent."""
      key: str
      version: Optional[str] = None
      bus_url: Optional[str] = ''
      bus_exchange_topic: Optional[str] = ''
      bus_management_url: Optional[str] = ''
      bus_vhost: Optional[str] = ''
      args: List[defintions.Arg] = dataclasses.field(default_factory=list)
      constraints: List[str] = dataclasses.field(default_factory=list)
      mounts: Optional[List[str]] = dataclasses.field(default_factory=list)
      restart_policy: str = 'any'
      mem_limit: Optional[int] = None
      open_ports: List[defintions.PortMapping] = dataclasses.field(default_factory=list)
      replicas: int = 1
      healthcheck_host: str = '0.0.0.0'
      healthcheck_port: int = 5000
  
      @property
      def container_image(self):
          """Agent image name."""
          image = self.key.replace('/', '_')
          logger.debug('Searching container name %s with version %s', image, self.version)
          client = docker.from_env()
          matching_tag_versions = []
          for img in client.images.list():
              for t in img.tags:
                  t_name, t_tag = t.split(':')
                  if t_name == image and self.version is None:
                      try:
                          matching_tag_versions.append(version.Version(t_tag[1:]))
                      except ValueError:
                          logger.warning('Invalid version %s', t_tag[1:])
                  elif t_name == image and self.version is not None:
                      if re.match(self.version, t_tag) is not None:
                          try:
                              matching_tag_versions.append(version.Version(t_tag[1:]))
                          except ValueError:
                              logger.warning('Invalid version %s', t_tag[1:])
  
          if not matching_tag_versions:
              return None
  
          matching_version = max(matching_tag_versions)
          return f'{image}:v{matching_version}'
  
      @classmethod
      def from_proto(cls, proto: bytes) -> 'AgentSettings':
          """Constructs an agent definition from a binary proto settings.
  
          Args:
              proto: binary proto settings file.
  
          Returns:
              AgentInstanceSettings object.
          """
          instance = agent_instance_settings_pb2.AgentInstanceSettings()
          instance.ParseFromString(proto)
          return cls(
              key=instance.key,
              bus_url=instance.bus_url,
              bus_exchange_topic=instance.bus_exchange_topic,
              bus_management_url=instance.bus_management_url,
              bus_vhost=instance.bus_vhost,
              args=[defintions.Arg(
                  name=a.name,
                  type=a.type,
                  value=a.value
              ) for a in instance.args],
              constraints=instance.constraints,
              mounts=instance.mounts,
              restart_policy=instance.restart_policy,
              mem_limit=instance.mem_limit,
              open_ports=[defintions.PortMapping(
                  source_port=p.source_port,
                  destination_port=p.destination_port
              ) for p in instance.open_ports],
              replicas=instance.replicas,
              healthcheck_host=instance.healthcheck_host,
              healthcheck_port=instance.healthcheck_port,
          )
  
      def to_raw_proto(self) -> bytes:
          """Transforms agent instance settings into a raw proto bytes.
  
          Returns:
              Bytes as a serialized proto.
          """
          instance = agent_instance_settings_pb2.AgentInstanceSettings()
          instance.key = self.key
          instance.bus_url = self.bus_url
          instance.bus_exchange_topic = self.bus_exchange_topic
          instance.bus_management_url = self.bus_management_url
          instance.bus_vhost = self.bus_vhost
  
          for arg in self.args:
              arg_instance = instance.args.add()
              arg_instance.name = arg.name
              arg_instance.type = arg.type
              arg_instance.value = arg.value
  
          instance.constraints.extend(self.constraints)
          instance.mounts.extend(self.mounts)
          instance.restart_policy = self.restart_policy
          if self.mem_limit is not None:
              instance.mem_limit = self.mem_limit
  
          for open_port in self.open_ports:
              open_port_instance = instance.open_ports.add()
              open_port_instance.source_port = open_port.source_port
              open_port_instance.destination_port = open_port.destination_port
  
          instance.replicas = self.replicas
          instance.healthcheck_host = self.healthcheck_host
          instance.healthcheck_port = self.healthcheck_port
  
          return instance.SerializeToString()
  
  
  @dataclasses.dataclass
  class AgentGroupDefinition:
      """Data class holding the attributes of an agent."""
      agents: List[AgentSettings]
  
      @classmethod
      def from_yaml(cls, group: io.FileIO):
          """Construct AgentGroupDefinition from yaml file.
  
          Args:
              group : agent group .yaml file.
          """
          agent_group_def = loader.load_agent_group_yaml(group)
          agent_settings = []
          for agent in agent_group_def['agents']:
              agent_def = AgentSettings(
                  key=agent.get('key'),
                  version=agent.get('version'),
                  args=[defintions.Arg(name=a.get('name'), description=a.get('description'), type=a.get('type'),
                                       value=a.get('value')) for a in
                        agent.get('args', [])],
                  constraints=agent.get('constraints'),
                  mounts=agent.get('mounts'),
                  restart_policy=agent.get('restart_policy'),
                  mem_limit=agent.get('mem_limit'),
                  open_ports=[defintions.PortMapping(source_port=p.get('src_port'), destination_port=p.get('dest_port'))
                              for p
                              in agent.get('open_ports', [])]
              )
  
              agent_settings.append(agent_def)
  
          return cls(agent_settings)
  