Migrer des règles de NAT VyOS vers NSX-V

Il y a quelques temps, j'ai procédé une modification majeure de l'architecture BGP d'un hébergeur. Cette modification imposait de transposer les règles de NAT présentent sur les routeurs BGP VyOS vers les routeurs NSX-V, plus proche du coeur de réseau. Il y avait environ 400 règles de NAT à transposer …

Il y a quelques temps, j'ai procédé une modification majeure de l'architecture BGP d'un hébergeur. Cette modification imposait de transposer les règles de NAT présentent sur les routeurs BGP VyOS vers les routeurs NSX-V, plus proche du coeur de réseau. Il y avait environ 400 règles de NAT à transposer. Ceux qui ont déjà fait du NAT sur NSX savent que réaliser ce genre de manipulation manuellement n'est pas une sinécure. L'interface n'est clairement pas adaptée pour ce genre de tâche. Par contre, NSX dispose d'une API plutot bien documentée. De l'autre coté, VyOS. VyOS est un système d'exploitation réseau, basé sur Debian avec des commandes similaires à ce qu'on peut trouver sur un routeur.

En discutant avec Daniil Baturin, principal mainteneur de VyOS, sur la meilleur façon de procéder, celui ci m'a indiqué la lib configtree qui permet de manipuler un fichier de configuration VyOS. Avec l'aide de cette lib, j'ai pu écrire ce script Python qui lit le fichier vyos.conf préalablement récupéré par mes soins et génére puis pousse les règles de NAT vers NSX-V.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
  import vyos.configtree
  import requests
  from requests.auth import HTTPBasicAuth

  # Set variables for edg_id and nsxmanager
  edge_id = 'edge-XXX'
  nsx_manager = 'nsxmanager.localdomain'

  def get_nat_dest_rule(config, path):
      '''
      From VyOS config section "nat destination rule X"
      Return the DNAT rule X as dict
      '''
      rule = dict()
      if config.exists(path + ["destination", "address"]):
          rule["dstaddr"] = config.return_value(path + ["destination", "address"])
      if config.exists(path + ["source", "address"]):
          rule["srcaddr"] = config.return_value(path + ["source", "address"])
      if config.exists(path + ["inbound-interface"]):
          rule["inbound-interface"] = config.return_value(path + ["inbound-interface"])
      if config.exists(path + ["log"]):
          rule["log"] = config.return_value(path + ["log"])
      if config.exists(path + ["protocol"]):
          rule["protocol"] = config.return_value(path + ["protocol"])
      if config.exists(path + ["description"]):
          rule["description"] = config.return_value(path + ["description"])
      if config.exists(path + ["translation", "address"]):
          rule["translateaddr"] = config.return_value(path + ["translation", "address"])

      return rule

  def get_nat_source_rule(config, path):
      '''
      From VyOS config section "nat source rule X"
      Return the SNAT rule X as dict
      '''
      rule = dict()
      if config.exists(path + ["source", "address"]):
          rule["srcaddr"] = config.return_value(path + ["source", "address"])
      if config.exists(path + ["destination", "address"]):
          rule["dstaddr"] = config.return_value(path + ["destination", "address"])
      if config.exists(path + ["outbound-interface"]):
          rule["outbound-interface"] = config.return_value(path + ["outbound-interface"])
      if config.exists(path + ["log"]):
          rule["log"] = config.return_value(path + ["log"])
      if config.exists(path + ["protocol"]):
          rule["protocol"] = config.return_value(path + ["protocol"])
      if config.exists(path + ["translation", "address"]):
          rule["translateaddr"] = config.return_value(path + ["translation", "address"])

      return rule

  def create_dstnat_payload(natdst):
      '''
      From all VyOS DNAT rules, create a XML payload that can be sent to NSX
      '''
      start_payload = "<natRules>"
      end_payload = "</natRules>"
      mid_payload = ""
      for rule in natdst.values():
          base_payload = "<natRule><action>dnat</action>\
                          <originalAddress>{0}</originalAddress>\
                          <translatedAddress>{1}</translatedAddress>\
                          <loggingEnabled>false</loggingEnabled>\
                          <enabled>true</enabled>\
                          <originalPort>any</originalPort>\
                          <translatedPort>any</translatedPort>".format(rule['dstaddr'], rule['translateaddr'])
          if "description" in rule:
              descr_payload = "<description>{0}</description>".format(rule['description'])
              base_payload = base_payload + descr_payload
          if "protocol" in rule:
              if rule['protocol'] == "all":
                  proto_payload = "<protocol>any</protocol>"
              else:
                  proto_payload = "<protocol>{0}</protocol>".format(rule['protocol'])

          else:
              proto_payload = "<protocol>any</protocol>"
          base_payload = base_payload + proto_payload
          if "srcaddr" in rule:
              src_payload = "<dnatMatchSourceAddress>{0}</dnatMatchSourceAddress>".format(rule['srcaddr'])
              base_payload = base_payload + src_payload

          mid_payload = mid_payload + base_payload + "</natRule>"

      payload = start_payload + mid_payload + end_payload
      return payload

  def create_srcnat_payload(natsrc):
      '''
      From all VyOS SNAT rules, create a XML payload that can be sent to NSX
      '''
      start_payload = "<natRules>"
      end_payload = "</natRules>"
      mid_payload = ""
      for rule in natsrc.values():
          base_payload = "<natRule><action>snat</action>\
                          <originalAddress>{0}</originalAddress>\
                          <translatedAddress>{1}</translatedAddress>\
                          <loggingEnabled>false</loggingEnabled>\
                          <enabled>true</enabled>\
                          <originalPort>any</originalPort>\
                          <translatedPort>any</translatedPort>".format(rule['srcaddr'], rule['translateaddr'])
          if "description" in rule:
              descr_payload = "<description>{0}</description>".format(rule['description'])
              base_payload = base_payload + descr_payload
          if "protocol" in rule:
              if rule['protocol'] == "all":
                  proto_payload = "<protocol>any</protocol>"
              else:
                  proto_payload = "<protocol>{0}</protocol>".format(rule['protocol'])

          else:
              proto_payload = "<protocol>any</protocol>"
          base_payload = base_payload + proto_payload
          if "dstaddr" in rule:
              src_payload = "<snatMatchDestinationAddress>{0}</snatMatchDestinationAddress>".format(rule['dstaddr'])
              base_payload = base_payload + src_payload

          mid_payload = mid_payload + base_payload + "</natRule>"

      payload = start_payload + mid_payload + end_payload
      return payload

  def main():
      with open("vyos.conf") as f:
          config_string = f.read()
      config = vyos.configtree.ConfigTree(config_string)

      # We start with DNAT rules
      destrulesid = config.list_nodes(["nat", "destination", "rule"])
      natdest_all = dict()
      for destruleid in destrulesid:
          '''
          We build a huge DNAT dict where keys are rules id and items are dict of rule parameter :
          '1': {'dstaddr': '1.2.3.4',
                'outbound-interface': 'eth1',
                'log': 'enable',
                'protocol': 'all',
                'translateaddr': '10.1.2.3'}
          '''
          path = ["nat", "destination", "rule {0}".format(destruleid)]
          natdest = get_nat_dest_rule(config, path)
          natdest_all[destruleid] = natdest
      #print(natdest_all)

      # Continue with SNAT rules
      srcrulesid = config.list_nodes(["nat", "source", "rule"])
      natsrc_all = dict()
      for srcruleid in srcrulesid:
          '''
          Same as the DNAT dict
          '''
          path = ["nat", "source", "rule {0}".format(srcruleid)]
          srcdest = get_nat_source_rule(config, path)
          natsrc_all[srcruleid] = srcdest
      #print(natsrc_all)

      # We now have two huge dictionnaries with DNAT and SNAT rules from VyOS
      # We are going to process them and POST them to NSX :crossed_fingers:

      # Replace edge-XXX with the right edge-id
      url = "https://{0}/api/4.0/edges/{1}/nat/config/rules".format(nsx_manager, edge_id)
      headers = {
      'Accept': 'application/xml',
      'Content-Type': 'application/xml'
      }

      dstnat_payload = create_dstnat_payload(natdest_all)
      srcnat_payload = create_srcnat_payload(natsrc_all)

      # This script is ment to be be run only once
      # Write your Vsphere login and password here
      # Don't commit your login and passord to git!!!!!!!!!
      username = 'user'
      password = 'password'

      # POST DNAT rules
      # This request append the rules to the existing ones
      #print(dstnat_payload)
      dstresponse = requests.request("POST", url, headers=headers, data = dstnat_payload, auth=HTTPBasicAuth(username, password), verify=False)
      print(dstresponse.headers)
      print(dstresponse.text.encode('utf8'))

      # POST SNAT rules
      # This request append the rules to the existing ones
      #print(srcnat_payload)
      srcresponse = requests.request("POST", url, headers=headers, data = srcnat_payload, auth=HTTPBasicAuth(username, password), verify=False)
      print(srcresponse.headers)
      print(srcresponse.text.encode('utf8'))

  if __name__ == '__main__':
      main()