go.ligato.io/vpp-agent/v3@v3.5.0/tests/robot/libraries/kubernetes/kube_config_gen.py (about)

     1  import yaml
     2  import os
     3  import math
     4  
     5  """Generates YAML config files for use with kubernetes."""
     6  
     7  
     8  def mac_hex(number):
     9      """Convert integer to hexadecimal for incrementing MAC addresses.
    10  
    11      :param number: Integer number less than 100.
    12      :type number: int
    13  
    14      :returns: 2-digit hexadecimal representation of the input number.
    15      :rtype: str
    16      """
    17  
    18      temp = hex(number)[2:]
    19      if number < 16:
    20          temp = "0{0}".format(temp)
    21      elif number > 99:
    22          raise NotImplementedError(
    23              "Incrementing MAC addresses only implemented up to 99.")
    24      else:
    25          pass
    26      return temp
    27  
    28  
    29  def yaml_replace_line(yaml_string, line_identifier, replacement):
    30      """Replace a single line in the specified string.
    31  
    32      :param yaml_string: String to replace in.
    33      :param line_identifier: A string which uniquely identifies the target line.
    34      :param replacement: String to replace the target line with.
    35      :type yaml_string: str
    36      :type line_identifier: str
    37      :type replacement: str
    38  
    39      :returns: Full string with the target line replaced.
    40      :rtype: str
    41      """
    42      for line in yaml_string.splitlines():
    43          if line_identifier in line:
    44              whitespace = len(line) - len(line.lstrip(" "))
    45              return yaml_string.replace(line, "{spaces}{content}".format(
    46                  spaces=" " * whitespace,
    47                  content=replacement
    48              ))
    49  
    50  
    51  class YamlConfigGenerator(object):
    52      """Config generator object."""
    53      def __init__(self, vnf_count, novpp_count, memif_per_vnf,
    54                   template_dir,
    55                   vswitch_image, vnf_image, sfc_image):
    56          """Initialize config generator with topology parameters.
    57  
    58          :param vnf_count: Number of VNF nodes.
    59          :param novpp_count: Number of non-VPP nodes.
    60          :param template_dir: Path to .yaml config templates.
    61          :type vnf_count: int
    62          :type novpp_count: int
    63          :type template_dir: str
    64          """
    65  
    66          self.vnf_count = int(vnf_count)
    67          self.novpp_count = int(novpp_count)
    68          self.memif_per_vnf = int(memif_per_vnf)
    69          self.images = {"vswitch": vswitch_image,
    70                         "vnf": vnf_image,
    71                         "sfc": sfc_image}
    72          self.templates = {}
    73          self.output = {}
    74          self.load_templates(template_dir)
    75  
    76          if self.novpp_count % self.memif_per_vnf != 0:
    77              raise NotImplementedError("Number of non-VPP containers must be"
    78                                        " a multiple of bridge domain count.")
    79  
    80      def load_templates(self, template_dir):
    81          """Open yaml template files and save into templates dictionary.
    82  
    83          :param template_dir: Path to directory containing the templates.
    84          :type template_dir: str
    85          """
    86  
    87          with open("{0}/sfc-k8.yaml".format(template_dir), "r") as sfc:
    88              self.templates["sfc"] = sfc.read()
    89          with open("{0}/vnf-vpp.yaml".format(template_dir), "r") as vnf:
    90              self.templates["vnf"] = vnf.read()
    91          with open("{0}/novpp.yaml".format(template_dir), "r") as novpp:
    92              self.templates["novpp"] = novpp.read()
    93          with open("{0}/vswitch.yaml".format(template_dir), "r") as vswitch:
    94              self.templates["vswitch"] = vswitch.read()
    95  
    96      def generate_config(self, output_path):
    97          """Generate topology config .yaml files for Kubernetes.
    98  
    99          :param output_path: Path to where the output files should be placed.
   100          :type output_path: str
   101  
   102          :returns: Generated topology in python dictionary format.
   103          :rtype: dict
   104          """
   105          topology = self.generate_sfc_config()
   106          self.generate_vnf_config()
   107          self.generate_novpp_config()
   108          self.generate_vswitch_config()
   109          self.write_config_files(output_path)
   110          return topology
   111  
   112      def generate_sfc_config(self):
   113          """Generate SFC configuration YAML file based on the desired topology.
   114  
   115          :returns: Generated topology in python dictionary format.
   116          :rtype: dict
   117  
   118          Topology description:
   119  
   120          One vswitch node, running VPP and agent.
   121  
   122          A number of VNFs equal to self.vnf_count, each running VPP and agent.
   123  
   124          A number of non-VPP nodes equal to self.novpp_count.
   125  
   126          A number of bridge domains configured on the vswitch equal
   127          to self.memif_per_vnf. Each bridge contains one memif interface
   128          to every VNF node. Non-VPP containers are distributed evenly across
   129          the bridges and connected using veth->af_packet.
   130          """
   131          entities_list = []
   132  
   133          for bridge_index in range(self.memif_per_vnf):
   134              entity = {
   135                  "name": "L2Bridge-{0}".format(bridge_index),
   136                  "description": "Vswitch L2 bridge.",
   137                  "type": 3,
   138                  "bd_parms": {
   139                      "learn": True,
   140                      "flood": True,
   141                      "forward": True,
   142                      "unknown_unicast_flood": True
   143                  },
   144                  "elements": [{
   145                      "container": "agent_vpp_vswitch",
   146                      "port_label": "L2Bridge-{0}".format(bridge_index),
   147                      "etcd_vpp_switch_key": "agent_vpp_vswitch",
   148                      "type": 5
   149                  }]
   150              }
   151  
   152              for vnf_index in range(self.vnf_count):
   153                  new_element = {
   154                      "container": "vnf-vpp-{index}".format(index=vnf_index),
   155                      "port_label": "vnf{0}_memif{1}".format(vnf_index,
   156                                                             bridge_index),
   157                      "mac_addr": "02:01:01:01:{0}:{1}".format(
   158                          mac_hex(bridge_index + 1),
   159                          mac_hex(vnf_index + 1)),
   160                      "ipv4_addr": "192.168.{0}.{1}".format(
   161                          bridge_index + 1,
   162                          vnf_index + 1),
   163                      "l2fib_macs": [
   164                          "192.168.{0}.{1}".format(
   165                              bridge_index + 1,
   166                              vnf_index + 1)
   167                      ],
   168                      "type": 2,
   169                      "etcd_vpp_switch_key": "agent_vpp_vswitch"
   170                  }
   171  
   172                  entity["elements"].append(new_element)
   173              novpp_range = int(math.ceil(
   174                  float(self.novpp_count) / float(self.memif_per_vnf)
   175              ))
   176  
   177              bridge_novpp_index = self.vnf_count + 1
   178              for novpp_index in range(
   179                              novpp_range * bridge_index,
   180                              (novpp_range * bridge_index) + novpp_range):
   181                  new_element = {
   182                      "container": "novpp-{index}".format(index=novpp_index),
   183                      "port_label": "veth_novpp{index}".format(index=novpp_index),
   184                      "mac_addr": "02:01:01:01:{0}:{1}".format(
   185                          mac_hex(bridge_index + 1),
   186                          mac_hex(novpp_index + self.vnf_count + 1)),
   187                      "ipv4_addr": "192.168.{0}.{1}".format(
   188                          bridge_index + 1,
   189                          bridge_novpp_index),
   190                      "type": 3,
   191                      "etcd_vpp_switch_key": "agent_vpp_vswitch"
   192                  }
   193                  entity["elements"].append(new_element)
   194                  bridge_novpp_index += 1
   195  
   196              entities_list.append(entity)
   197  
   198          output = ""
   199          for line in yaml.dump(
   200                  entities_list,
   201                  default_flow_style=False
   202          ).splitlines():
   203              output += " "*6 + line + "\n"
   204  
   205          template = self.templates["sfc"]
   206          if "---" in template:
   207              sections = template.split("---")
   208              for section in sections:
   209                  if "sfc_entities:" in section:
   210                      output = template.replace(section, section + output)
   211          else:
   212              output = template + output
   213  
   214          self.output["sfc"] = yaml_replace_line(
   215              output,
   216              "image:",
   217              "image: {0}".format(self.images["sfc"]))
   218  
   219          topology = []
   220          for bridge_segment in entities_list:
   221              segment_topology = {"vnf": [], "novpp": []}
   222              for element in bridge_segment["elements"]:
   223                  if element["container"].startswith("vnf-vpp"):
   224                      segment_topology["vnf"].append(
   225                          {"name": element["container"],
   226                           "ip": element["ipv4_addr"]})
   227                  elif element["container"].startswith("novpp"):
   228                      segment_topology["novpp"].append(
   229                          {"name": element["container"],
   230                           "ip": element["ipv4_addr"]})
   231              topology.append(segment_topology)
   232          return topology
   233  
   234      def generate_vnf_config(self):
   235          """Read VNF configuration YAML file and modify pod replication count
   236          and image name.
   237          """
   238  
   239          template = self.templates["vnf"]
   240          output = yaml_replace_line(
   241              template,
   242              "replicas:",
   243              "replicas: {0}".format(self.vnf_count))
   244          output = yaml_replace_line(
   245              output,
   246              "image:",
   247              "image: {0}".format(self.images["vnf"]))
   248          self.output["vnf"] = output
   249  
   250      def generate_novpp_config(self):
   251          """Read non-VPP configuration YAML file and modify pod replication count.
   252          """
   253  
   254          template = self.templates["novpp"]
   255          output = yaml_replace_line(
   256              template,
   257              "replicas:",
   258              "replicas: {0}".format(self.novpp_count))
   259          self.output["novpp"] = output
   260  
   261      def generate_vswitch_config(self):
   262          """Read vswitch configuration yaml file and modify image name.
   263          """
   264          template = self.templates["vswitch"]
   265          output = yaml_replace_line(
   266              template,
   267              "image:",
   268              "image: {0}".format(self.images["vswitch"]))
   269          self.output["vswitch"] = output
   270  
   271      def write_config_files(self, output_path):
   272          """Write the generated config files to the specified directory.
   273  
   274          :param output_path: Path to write files to.
   275          :type output_path: str
   276          """
   277          if not os.path.exists(output_path):
   278              os.makedirs(output_path)
   279  
   280          with open("{0}/sfc.yaml".format(output_path), "w") as sfc:
   281              sfc.write(self.output["sfc"])
   282          with open("{0}/vnf.yaml".format(output_path), "w") as vnf:
   283              vnf.write(self.output["vnf"])
   284          with open("{0}/novpp.yaml".format(output_path), "w") as novpp:
   285              novpp.write(self.output["novpp"])
   286          with open("{0}/vswitch.yaml".format(output_path), "w") as vswitch:
   287              vswitch.write(self.output["vswitch"])
   288  
   289  
   290  def generate_config(vnf_count, novpp_count, memif_per_vnf,
   291                      template_path, output_path,
   292                      vswitch_image, vnf_image, sfc_image):
   293      """Generate YAML config using the arguments and templates provided. Write
   294      generated files to the specified path."""
   295      generator = YamlConfigGenerator(
   296          vnf_count, novpp_count, memif_per_vnf,
   297          template_path,
   298          vswitch_image, vnf_image, sfc_image)
   299  
   300      topology = generator.generate_config(output_path)
   301      return topology