412 lines
17 KiB
Python
Executable File
412 lines
17 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# Copyright (c) 2017 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# This script will migrate away from using vlan-tagged subnets,
|
|
# to using separate networks with their compute ports trunked
|
|
# from the network the vlan-tagged subnet was on.
|
|
# Once all of the compute nodes are updates, the old vlan-tagged
|
|
# subnets, as well as all of the ports on them, will be deleted.
|
|
import os
|
|
import psycopg2
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
|
|
from psycopg2.extras import RealDictCursor
|
|
|
|
from controllerconfig.common import log
|
|
|
|
LOG = log.get_logger(__name__)
|
|
|
|
|
|
def main():
|
|
action = None
|
|
from_release = None
|
|
to_release = None # noqa
|
|
arg = 1
|
|
while arg < len(sys.argv):
|
|
if arg == 1:
|
|
from_release = sys.argv[arg]
|
|
elif arg == 2:
|
|
to_release = sys.argv[arg] # noqa
|
|
elif arg == 3:
|
|
action = sys.argv[arg]
|
|
else:
|
|
print ("Invalid option %s." % sys.argv[arg])
|
|
return 1
|
|
arg += 1
|
|
|
|
log.configure()
|
|
|
|
if from_release == "17.06" and action == "migrate":
|
|
try:
|
|
migrate_vlan()
|
|
except Exception as ex:
|
|
LOG.exception(ex)
|
|
print ex
|
|
return 1
|
|
|
|
if from_release == "17.06" and action == "activate":
|
|
try:
|
|
cleanup_neutron_vlan_subnets()
|
|
except Exception as ex:
|
|
LOG.exception(ex)
|
|
print ex
|
|
return 1
|
|
|
|
|
|
def run_cmd(cur, cmd):
|
|
cur.execute(cmd)
|
|
|
|
|
|
def run_cmd_postgres(sub_cmd):
|
|
"""
|
|
This executes the given command as user postgres. This is necessary when
|
|
this script is run as root, which is the case on an upgrade activation.
|
|
"""
|
|
error_output = open(os.devnull, 'w')
|
|
cmd = ("sudo -u postgres psql -d neutron -c \"%s\"" % sub_cmd)
|
|
LOG.info("Executing '%s'" % cmd)
|
|
subprocess.check_call([cmd], shell=True, stderr=error_output)
|
|
|
|
|
|
def migrate_vlan():
|
|
conn = psycopg2.connect("dbname=neutron user=postgres")
|
|
with conn:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
create_new_networks(cur)
|
|
|
|
|
|
def cleanup_neutron_vlan_subnets():
|
|
"""
|
|
This function cleans up data leftover from migrating away from using
|
|
vlan-tagged subnets. Specifically, it deletes all non-compute ports
|
|
on vlan-tagged subnets, as well as all vlan-tagged subnets.
|
|
"""
|
|
cmd = ("DELETE FROM ports WHERE id in"
|
|
" (SELECT port_id FROM ipallocations AS ipa"
|
|
" JOIN subnets AS s ON ipa.subnet_id = s.id"
|
|
" where s.vlan_id!=0)"
|
|
" AND device_owner not like 'compute:%';")
|
|
run_cmd_postgres(cmd)
|
|
|
|
cmd = "DELETE FROM subnets WHERE vlan_id != 0;"
|
|
run_cmd_postgres(cmd)
|
|
|
|
|
|
def create_new_networks(cur):
|
|
"""
|
|
This function creates new networks for each network segment belonging to
|
|
a vlan-tagged subnet, and clones those subnets minus the vlan ID.
|
|
For each of those cloned subnets, it also clones all of the ports on them,
|
|
as well as all of the IP allocations, and the bindings
|
|
"""
|
|
cmd = ("SELECT s.vlan_id, s.network_id, m2ss.network_type,"
|
|
" m2ss.physical_network, m2ss.segmentation_id FROM subnets AS s"
|
|
" JOIN ml2_subnet_segments AS m2ss ON s.id = m2ss.subnet_id"
|
|
" WHERE s.vlan_id != 0 GROUP BY s.vlan_id, s.network_id,"
|
|
" m2ss.network_type, m2ss.physical_network, m2ss.segmentation_id;")
|
|
run_cmd(cur, cmd)
|
|
networks_to_create = []
|
|
while True:
|
|
network = cur.fetchone()
|
|
if network is None:
|
|
break
|
|
networks_to_create.append(network)
|
|
|
|
for network in networks_to_create:
|
|
create_and_populate_network(cur, network)
|
|
|
|
|
|
def create_standard_attribute(cur, name):
|
|
"""
|
|
This function creates new standard attribute entries to be used by copied
|
|
data.
|
|
"""
|
|
cmd = ("INSERT INTO standardattributes (resource_type)"
|
|
" VALUES ('%s') RETURNING id") %\
|
|
(name,)
|
|
run_cmd(cur, cmd)
|
|
return cur.fetchone()['id']
|
|
|
|
|
|
def create_and_populate_network(cur, network):
|
|
"""
|
|
This function takes a network segment, and copies all the data on that
|
|
network segment to a newly-created network. For each compute port on the
|
|
original network, a port trunk should be created from the original port
|
|
as a parent, to the new port as a subport. This relaces the vlan id being
|
|
set on an individual subnet.
|
|
"""
|
|
vlan_id = network['vlan_id']
|
|
network_type = network['network_type']
|
|
old_network_id = network['network_id']
|
|
# This new network ID should be the same as neutron passes to vswitch for
|
|
# the network-uuid of the network segment for the vlan-tagged subnet.
|
|
network_suffix = "vlan%s" % vlan_id
|
|
new_network_id = uuid.uuid5(uuid.UUID(old_network_id), network_suffix)
|
|
new_networksegment_id = uuid.uuid4()
|
|
cmd = ("INSERT INTO networks (project_id, id, name, status,"
|
|
"admin_state_up, vlan_transparent, standard_attr_id,"
|
|
" availability_zone_hints)"
|
|
" (SELECT project_id, '%s',"
|
|
" CONCAT_WS('-VLAN%d', NULLIF(name,''), ''), status,"
|
|
" admin_state_up, vlan_transparent, '%s', availability_zone_hints"
|
|
" FROM networks WHERE id = '%s') RETURNING id;") %\
|
|
(new_network_id, vlan_id,
|
|
create_standard_attribute(cur, 'networks'), old_network_id)
|
|
run_cmd(cur, cmd)
|
|
old_network_id = network['network_id']
|
|
new_network_id = cur.fetchone()['id']
|
|
|
|
cmd = ("INSERT INTO networksegments (id, network_id, network_type,"
|
|
" physical_network, segmentation_id, is_dynamic, segment_index,"
|
|
" standard_attr_id, name)"
|
|
" VALUES('%s','%s','%s','%s','%s','%s','%s','%s','%s')") %\
|
|
(new_networksegment_id, new_network_id, network_type,
|
|
network['physical_network'], network['segmentation_id'],
|
|
'f', '0', create_standard_attribute(cur, 'networksegments'), '')
|
|
run_cmd(cur, cmd)
|
|
|
|
# Get a list of vlan-tagged subnets on the network we are copying.
|
|
# For each of these subnets, we loop through and copy them, and then loop
|
|
# through the ip allocations on them and copy those ip allocations, along
|
|
# with the ports that are in those ip allocations.
|
|
sub_cmd = ("SELECT id FROM subnets"
|
|
" WHERE vlan_id = '%s' AND network_id='%s'") %\
|
|
(vlan_id, old_network_id)
|
|
|
|
# Copy the subnets to the new network
|
|
run_cmd(cur, sub_cmd)
|
|
subnets = cur.fetchall()
|
|
subnet_copies = {}
|
|
for subnet in subnets:
|
|
old_subnet_id = subnet['id']
|
|
new_subnet_id = uuid.uuid4()
|
|
new_ml2_subnet_segment_id = uuid.uuid4()
|
|
subnet_copies[old_subnet_id] = new_subnet_id
|
|
cmd = ("INSERT INTO subnets"
|
|
" (project_id, id, name, network_id, ip_version, cidr,"
|
|
" gateway_ip, enable_dhcp, ipv6_ra_mode, ipv6_address_mode,"
|
|
" subnetpool_id, vlan_id, standard_attr_id, segment_id)"
|
|
" (SELECT project_id, '%s', name, '%s', ip_version, cidr,"
|
|
" gateway_ip, enable_dhcp, ipv6_ra_mode, ipv6_address_mode,"
|
|
" subnetpool_id, 0, '%s', segment_id"
|
|
" FROM subnets WHERE id='%s')") %\
|
|
(new_subnet_id, new_network_id,
|
|
create_standard_attribute(cur, 'subnets'), old_subnet_id)
|
|
run_cmd(cur, cmd)
|
|
cmd = ("INSERT INTO ml2_subnet_segments"
|
|
" (id, subnet_id, network_type, physical_network,"
|
|
" segmentation_id, is_dynamic, segment_index)"
|
|
" (SELECT '%s', '%s', network_type, physical_network,"
|
|
" segmentation_id, is_dynamic, segment_index"
|
|
" FROM ml2_subnet_segments WHERE subnet_id='%s')") %\
|
|
(new_ml2_subnet_segment_id, new_subnet_id, old_subnet_id)
|
|
run_cmd(cur, cmd)
|
|
duplicate_ipam_subnets(cur, old_subnet_id, new_subnet_id)
|
|
duplicate_ipallocationpools(cur, old_subnet_id, new_subnet_id)
|
|
|
|
# Copy the ports that are related to vlan subnets such that those new
|
|
# ports are directly attached to the network that was created to replace
|
|
# the vlan subnet. We ignore DHCP ports because since both the vlan
|
|
# subnet and the new network will share the same provider network we do
|
|
# not want 2 ports with the same IP to exist simultaneously. Instead,
|
|
# we let the DHCP server allocate this port when it notices that it is
|
|
# missing which will result in a new IP allocation and should not
|
|
# interfere with any existing allocations because they have all been
|
|
# cloned onto the new network.
|
|
cmd = ("SELECT DISTINCT port_id FROM ipallocations"
|
|
" LEFT JOIN ports AS p ON p.id = ipallocations.port_id"
|
|
" WHERE p.device_owner != 'network:dhcp'"
|
|
" AND subnet_id IN (%s)") % sub_cmd
|
|
run_cmd(cur, cmd)
|
|
ports_to_copy = cur.fetchall()
|
|
port_copies = {}
|
|
for port in ports_to_copy:
|
|
old_port_id = port['port_id']
|
|
new_port_id = uuid.uuid4()
|
|
port_copies[old_port_id] = new_port_id
|
|
cmd = ("INSERT INTO ports (project_id, id, name, network_id,"
|
|
" mac_address, admin_state_up, status, device_id, device_owner,"
|
|
" standard_attr_id, ip_allocation)"
|
|
" (SELECT project_id, '%s',"
|
|
" CONCAT_WS('-VLAN%d', NULLIF(name,''), ''), '%s',"
|
|
" mac_address, admin_state_up, status, device_id, device_owner,"
|
|
"'%s', ip_allocation FROM ports WHERE id = '%s')"
|
|
" RETURNING id, device_owner") %\
|
|
(new_port_id, vlan_id, new_network_id,
|
|
create_standard_attribute(cur, 'ports'), old_port_id)
|
|
run_cmd(cur, cmd)
|
|
new_port = cur.fetchone()
|
|
new_port_owner = new_port['device_owner']
|
|
cmd = ("INSERT INTO ml2_port_bindings"
|
|
" (port_id, host, vif_type, vnic_type, profile,"
|
|
" vif_details, vif_model, mac_filtering, mtu)"
|
|
" (SELECT '%s', host, vif_type, vnic_type, profile,"
|
|
" vif_details, vif_model, mac_filtering, mtu"
|
|
" FROM ml2_port_bindings where port_id='%s')") %\
|
|
(new_port_id, old_port_id)
|
|
run_cmd(cur, cmd)
|
|
cmd = ("INSERT INTO ml2_port_binding_levels"
|
|
" (port_id, host, level, driver, segment_id)"
|
|
" (SELECT '%s', host, level, driver, '%s'"
|
|
" FROM ml2_port_binding_levels WHERE port_id='%s')") %\
|
|
(new_port_id, new_networksegment_id, old_port_id)
|
|
run_cmd(cur, cmd)
|
|
if new_port_owner.startswith('compute:'):
|
|
trunk_id = create_port_trunk(cur, old_port_id)
|
|
create_subport(cur, trunk_id, new_port_id, 'vlan', vlan_id)
|
|
elif new_port_owner.startswith('network:router'):
|
|
cmd = ("INSERT INTO routerports (router_id, port_id, port_type)"
|
|
" (SELECT router_id, '%s', port_type FROM routerports"
|
|
" WHERE port_id = '%s')") %\
|
|
(new_port_id, old_port_id)
|
|
run_cmd(cur, cmd)
|
|
elif new_port_owner == 'network:dhcp':
|
|
# Set new port's device_id to DEVICE_ID_RESERVED_DHCP_PORT,
|
|
# so that it is used by dhcp agent for new subnet.
|
|
cmd = ("UPDATE ports SET device_id='reserved_dhcp_port'"
|
|
" WHERE id='%s'") %\
|
|
(new_port_id,)
|
|
run_cmd(cur, cmd)
|
|
|
|
# Copy the ipallocations
|
|
cmd = ("SELECT * FROM ipallocations WHERE network_id='%s'") %\
|
|
(old_network_id)
|
|
run_cmd(cur, cmd)
|
|
ipallocations = cur.fetchall()
|
|
for ipallocation in ipallocations:
|
|
old_ip_address = ipallocation['ip_address']
|
|
old_port_id = ipallocation['port_id']
|
|
old_subnet_id = ipallocation['subnet_id']
|
|
new_port_id = port_copies.get(old_port_id)
|
|
new_subnet_id = subnet_copies.get(old_subnet_id)
|
|
if not new_port_id or not new_subnet_id:
|
|
continue
|
|
cmd = ("INSERT INTO ipallocations"
|
|
" (port_id, ip_address, subnet_id, network_id)"
|
|
" VALUES ('%s', '%s', '%s', '%s')") %\
|
|
(new_port_id, old_ip_address, new_subnet_id, new_network_id)
|
|
run_cmd(cur, cmd)
|
|
|
|
# Copy the DHCP network agent bindings so that the new networks are
|
|
# initial scheduled to the same agents as the vlan subnets they are
|
|
# replacing. The alternative is that all new networks are initially
|
|
# unscheduled and they may all get scheduled to the same agent when any
|
|
# of the agents query for new networks to service.
|
|
cmd = ("SELECT * FROM networkdhcpagentbindings WHERE network_id='%s'" %
|
|
old_network_id)
|
|
run_cmd(cur, cmd)
|
|
bindings = cur.fetchall()
|
|
for binding in bindings:
|
|
agent_id = binding['dhcp_agent_id']
|
|
cmd = ("INSERT INTO networkdhcpagentbindings"
|
|
" (network_id, dhcp_agent_id)"
|
|
" VALUES ('%s', '%s')" %
|
|
(new_network_id, agent_id))
|
|
run_cmd(cur, cmd)
|
|
|
|
|
|
def duplicate_ipam_subnets(cur, old_neutron_subnet_id, new_neutron_subnet_id):
|
|
cmd = ("SELECT id from ipamsubnets WHERE neutron_subnet_id='%s'") %\
|
|
(old_neutron_subnet_id)
|
|
run_cmd(cur, cmd)
|
|
ipamsubnets = cur.fetchall()
|
|
for ipamsubnet in ipamsubnets:
|
|
old_ipamsubnet_id = ipamsubnet['id']
|
|
new_ipamsubnet_id = uuid.uuid4()
|
|
cmd = ("INSERT INTO ipamsubnets (id, neutron_subnet_id)"
|
|
" VALUES ('%s', '%s')") %\
|
|
(new_ipamsubnet_id, new_neutron_subnet_id)
|
|
run_cmd(cur, cmd)
|
|
cmd = ("SELECT * from ipamallocationpools"
|
|
" WHERE ipam_subnet_id='%s'") %\
|
|
(old_ipamsubnet_id)
|
|
run_cmd(cur, cmd)
|
|
ipamallocationpools = cur.fetchall()
|
|
for ipamallocationpool in ipamallocationpools:
|
|
new_ipamallocationpool_id = uuid.uuid4()
|
|
first_ip = ipamallocationpool['first_ip']
|
|
last_ip = ipamallocationpool['last_ip']
|
|
cmd = ("INSERT INTO ipamallocationpools"
|
|
" (id, ipam_subnet_id, first_ip, last_ip)"
|
|
" VALUES ('%s', '%s', '%s', '%s')") %\
|
|
(new_ipamallocationpool_id, new_ipamsubnet_id,
|
|
first_ip, last_ip)
|
|
run_cmd(cur, cmd)
|
|
cmd = ("INSERT INTO ipamallocations"
|
|
" (ip_address, status, ipam_subnet_id)"
|
|
" (SELECT ip_address, status, '%s' FROM ipamallocations"
|
|
" WHERE ipam_subnet_id='%s')") %\
|
|
(new_ipamsubnet_id, old_ipamsubnet_id)
|
|
run_cmd(cur, cmd)
|
|
|
|
|
|
def duplicate_ipallocationpools(cur, old_subnet_id, new_subnet_id):
|
|
cmd = ("SELECT * from ipallocationpools WHERE subnet_id='%s'") %\
|
|
(old_subnet_id)
|
|
run_cmd(cur, cmd)
|
|
ipallocationpools = cur.fetchall()
|
|
for ipallocationpool in ipallocationpools:
|
|
new_ipallocationpool_id = uuid.uuid4()
|
|
first_ip = ipallocationpool['first_ip']
|
|
last_ip = ipallocationpool['last_ip']
|
|
cmd = ("INSERT INTO ipallocationpools"
|
|
" (id, subnet_id, first_ip, last_ip)"
|
|
" VALUES ('%s', '%s', '%s', '%s')") %\
|
|
(new_ipallocationpool_id, new_subnet_id,
|
|
first_ip, last_ip)
|
|
run_cmd(cur, cmd)
|
|
|
|
|
|
def create_port_trunk(cur, port_id):
|
|
"""
|
|
This function will create a trunk off of a given port if there doesn't
|
|
already exist a trunk off of that port. This port should be a compute
|
|
port, where this is to replace a vlan-tagged subnet on that port.
|
|
"""
|
|
# create trunk if not exists
|
|
cmd = ("SELECT id FROM trunks WHERE port_id = '%s'") %\
|
|
(port_id)
|
|
run_cmd(cur, cmd)
|
|
trunk = cur.fetchone()
|
|
if trunk:
|
|
return trunk['id']
|
|
|
|
cmd = ("INSERT INTO trunks (admin_state_up, project_id, id, name, port_id,"
|
|
" status, standard_attr_id)"
|
|
" (SELECT admin_state_up, project_id, '%s', name, id, status, '%s'"
|
|
" FROM ports WHERE id = '%s') RETURNING id") %\
|
|
(uuid.uuid4(), create_standard_attribute(cur, 'trunks'), port_id)
|
|
run_cmd(cur, cmd)
|
|
trunk = cur.fetchone()
|
|
return trunk['id']
|
|
|
|
|
|
def create_subport(cur, trunk_id, subport_id, segmentation_type,
|
|
segmentation_id):
|
|
"""
|
|
Create a subport off of a given network trunk.
|
|
The segmentation_id should be the vlan id as visible to the guest,
|
|
not the segmentation id of the network segment.
|
|
"""
|
|
cmd = ("INSERT INTO subports"
|
|
" (port_id, trunk_id, segmentation_type, segmentation_id)"
|
|
" VALUES ('%s', '%s','%s','%s')") %\
|
|
(subport_id, trunk_id, segmentation_type, segmentation_id)
|
|
run_cmd(cur, cmd)
|
|
cmd = ("UPDATE ports SET device_id='', device_owner='trunk:subport'"
|
|
" WHERE id='%s'") % subport_id
|
|
run_cmd(cur, cmd)
|
|
vif_details = '{\"port_filter\": true, \"vhostuser_enabled\": false}'
|
|
cmd = ("UPDATE ml2_port_bindings SET vif_model='',vif_details='%s'"
|
|
" WHERE port_id='%s'" % (vif_details, subport_id))
|
|
run_cmd(cur, cmd)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|