Josh's networking refactor, modified to work with projects
This commit is contained in:
@@ -1,25 +1,11 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
# Copyright [2010] [Anso Labs, LLC]
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
|
import os
|
||||||
|
import nova.utils
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from nova import utils
|
|
||||||
|
|
||||||
# todo(ja): does the definition of network_path belong here?
|
# todo(ja): does the definition of network_path belong here?
|
||||||
|
|
||||||
from nova import flags
|
from nova import flags
|
||||||
@@ -30,13 +16,13 @@ def execute(cmd):
|
|||||||
logging.debug("FAKE NET: %s" % cmd)
|
logging.debug("FAKE NET: %s" % cmd)
|
||||||
return "fake", 0
|
return "fake", 0
|
||||||
else:
|
else:
|
||||||
utils.execute(cmd)
|
return nova.utils.execute(cmd)
|
||||||
|
|
||||||
def runthis(desc, cmd):
|
def runthis(desc, cmd):
|
||||||
if FLAGS.fake_network:
|
if FLAGS.fake_network:
|
||||||
execute(cmd)
|
return execute(cmd)
|
||||||
else:
|
else:
|
||||||
utils.runthis(desc,cmd)
|
return nova.utils.runthis(desc,cmd)
|
||||||
|
|
||||||
def Popen(cmd):
|
def Popen(cmd):
|
||||||
if FLAGS.fake_network:
|
if FLAGS.fake_network:
|
||||||
@@ -61,44 +47,46 @@ def bind_public_ip(ip, interface):
|
|||||||
|
|
||||||
def vlan_create(net):
|
def vlan_create(net):
|
||||||
""" create a vlan on on a bridge device unless vlan already exists """
|
""" create a vlan on on a bridge device unless vlan already exists """
|
||||||
if not device_exists("vlan%s" % net.vlan):
|
if not device_exists("vlan%s" % net['vlan']):
|
||||||
|
logging.debug("Starting VLAN inteface for %s network", (net['vlan']))
|
||||||
execute("sudo vconfig set_name_type VLAN_PLUS_VID_NO_PAD")
|
execute("sudo vconfig set_name_type VLAN_PLUS_VID_NO_PAD")
|
||||||
execute("sudo vconfig add %s %s" % (net.bridge_dev, net.vlan))
|
execute("sudo vconfig add %s %s" % (FLAGS.bridge_dev, net['vlan']))
|
||||||
execute("sudo ifconfig vlan%s up" % (net.vlan))
|
execute("sudo ifconfig vlan%s up" % (net['vlan']))
|
||||||
|
|
||||||
def bridge_create(net):
|
def bridge_create(net):
|
||||||
""" create a bridge on a vlan unless it already exists """
|
""" create a bridge on a vlan unless it already exists """
|
||||||
if not device_exists(net.bridge_name):
|
if not device_exists(net['bridge_name']):
|
||||||
execute("sudo brctl addbr %s" % (net.bridge_name))
|
logging.debug("Starting Bridge inteface for %s network", (net['vlan']))
|
||||||
|
execute("sudo brctl addbr %s" % (net['bridge_name']))
|
||||||
# execute("sudo brctl setfd %s 0" % (net.bridge_name))
|
# execute("sudo brctl setfd %s 0" % (net.bridge_name))
|
||||||
# execute("sudo brctl setageing %s 10" % (net.bridge_name))
|
# execute("sudo brctl setageing %s 10" % (net.bridge_name))
|
||||||
execute("sudo brctl stp %s off" % (net.bridge_name))
|
execute("sudo brctl stp %s off" % (net['bridge_name']))
|
||||||
execute("sudo brctl addif %s vlan%s" % (net.bridge_name, net.vlan))
|
execute("sudo brctl addif %s vlan%s" % (net['bridge_name'], net['vlan']))
|
||||||
if net.bridge_gets_ip:
|
if net.bridge_gets_ip:
|
||||||
execute("sudo ifconfig %s %s broadcast %s netmask %s up" % \
|
execute("sudo ifconfig %s %s broadcast %s netmask %s up" % \
|
||||||
(net.bridge_name, net.gateway, net.broadcast, net.netmask))
|
(net['bridge_name'], net.gateway, net.broadcast, net.netmask))
|
||||||
confirm_rule("FORWARD --in-interface %s -j ACCEPT" % (net.bridge_name))
|
confirm_rule("FORWARD --in-interface %s -j ACCEPT" % (net['bridge_name']))
|
||||||
else:
|
else:
|
||||||
execute("sudo ifconfig %s up" % net.bridge_name)
|
execute("sudo ifconfig %s up" % net['bridge_name'])
|
||||||
|
|
||||||
def dnsmasq_cmd(net):
|
def dnsmasq_cmd(net):
|
||||||
cmd = ['sudo dnsmasq',
|
cmd = ['sudo dnsmasq',
|
||||||
' --strict-order',
|
' --strict-order',
|
||||||
' --bind-interfaces',
|
' --bind-interfaces',
|
||||||
' --conf-file=',
|
' --conf-file=',
|
||||||
' --pid-file=%s' % dhcp_file(net.vlan, 'pid'),
|
' --pid-file=%s' % dhcp_file(net['vlan'], 'pid'),
|
||||||
' --listen-address=%s' % net.dhcp_listen_address,
|
' --listen-address=%s' % net.dhcp_listen_address,
|
||||||
' --except-interface=lo',
|
' --except-interface=lo',
|
||||||
' --dhcp-range=%s,%s,120s' % (net.dhcp_range_start, net.dhcp_range_end),
|
' --dhcp-range=%s,static,120s' % (net.dhcp_range_start),
|
||||||
' --dhcp-lease-max=61',
|
' --dhcp-lease-max=61',
|
||||||
' --dhcp-hostsfile=%s' % dhcp_file(net.vlan, 'conf'),
|
' --dhcp-hostsfile=%s' % dhcp_file(net['vlan'], 'conf'),
|
||||||
' --dhcp-leasefile=%s' % dhcp_file(net.vlan, 'leases')]
|
' --dhcp-leasefile=%s' % dhcp_file(net['vlan'], 'leases')]
|
||||||
return ''.join(cmd)
|
return ''.join(cmd)
|
||||||
|
|
||||||
def hostDHCP(network, host):
|
def hostDHCP(network, host, mac):
|
||||||
idx = host['address'].split(".")[-1] # Logically, the idx of instances they've launched in this net
|
idx = host.split(".")[-1] # Logically, the idx of instances they've launched in this net
|
||||||
return "%s,%s-%s-%s.novalocal,%s" % \
|
return "%s,%s-%s-%s.novalocal,%s" % \
|
||||||
(host['mac'], host['user_id'], network.vlan, idx, host['address'])
|
(mac, network['user_id'], network['vlan'], idx, host)
|
||||||
|
|
||||||
# todo(ja): if the system has restarted or pid numbers have wrapped
|
# todo(ja): if the system has restarted or pid numbers have wrapped
|
||||||
# then you cannot be certain that the pid refers to the
|
# then you cannot be certain that the pid refers to the
|
||||||
@@ -111,9 +99,9 @@ def start_dnsmasq(network):
|
|||||||
if a dnsmasq instance is already running then send a HUP
|
if a dnsmasq instance is already running then send a HUP
|
||||||
signal causing it to reload, otherwise spawn a new instance
|
signal causing it to reload, otherwise spawn a new instance
|
||||||
"""
|
"""
|
||||||
with open(dhcp_file(network.vlan, 'conf'), 'w') as f:
|
with open(dhcp_file(network['vlan'], 'conf'), 'w') as f:
|
||||||
for host_name in network.hosts:
|
for host_name in network.hosts:
|
||||||
f.write("%s\n" % hostDHCP(network, network.hosts[host_name]))
|
f.write("%s\n" % hostDHCP(network, host_name, network.hosts[host_name]))
|
||||||
|
|
||||||
pid = dnsmasq_pid_for(network)
|
pid = dnsmasq_pid_for(network)
|
||||||
|
|
||||||
@@ -123,12 +111,11 @@ def start_dnsmasq(network):
|
|||||||
# correct dnsmasq process
|
# correct dnsmasq process
|
||||||
try:
|
try:
|
||||||
os.kill(pid, signal.SIGHUP)
|
os.kill(pid, signal.SIGHUP)
|
||||||
return
|
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
logging.debug("Hupping dnsmasq threw %s", e)
|
logging.debug("Hupping dnsmasq threw %s", e)
|
||||||
|
|
||||||
# otherwise delete the existing leases file and start dnsmasq
|
# otherwise delete the existing leases file and start dnsmasq
|
||||||
lease_file = dhcp_file(network.vlan, 'leases')
|
lease_file = dhcp_file(network['vlan'], 'leases')
|
||||||
if os.path.exists(lease_file):
|
if os.path.exists(lease_file):
|
||||||
os.unlink(lease_file)
|
os.unlink(lease_file)
|
||||||
|
|
||||||
@@ -156,7 +143,7 @@ def dnsmasq_pid_for(network):
|
|||||||
if machine has rebooted pid might be incorrect (caller should check)
|
if machine has rebooted pid might be incorrect (caller should check)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pid_file = dhcp_file(network.vlan, 'pid')
|
pid_file = dhcp_file(network['vlan'], 'pid')
|
||||||
|
|
||||||
if os.path.exists(pid_file):
|
if os.path.exists(pid_file):
|
||||||
with open(pid_file, 'r') as f:
|
with open(pid_file, 'r') as f:
|
||||||
|
|||||||
@@ -17,15 +17,16 @@
|
|||||||
Classes for network control, including VLANs, DHCP, and IP allocation.
|
Classes for network control, including VLANs, DHCP, and IP allocation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
# TODO(termie): clean up these imports
|
# TODO(termie): clean up these imports
|
||||||
from nova import vendor
|
from nova import vendor
|
||||||
import IPy
|
import IPy
|
||||||
|
|
||||||
from nova import datastore
|
from nova import datastore
|
||||||
|
import nova.exception
|
||||||
from nova.compute import exception
|
from nova.compute import exception
|
||||||
from nova import flags
|
from nova import flags
|
||||||
from nova import utils
|
from nova import utils
|
||||||
@@ -34,145 +35,129 @@ from nova.auth import users
|
|||||||
import linux_net
|
import linux_net
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
flags.DEFINE_string('net_libvirt_xml_template',
|
|
||||||
utils.abspath('compute/net.libvirt.xml.template'),
|
|
||||||
'Template file for libvirt networks')
|
|
||||||
flags.DEFINE_string('networks_path', utils.abspath('../networks'),
|
flags.DEFINE_string('networks_path', utils.abspath('../networks'),
|
||||||
'Location to keep network config files')
|
'Location to keep network config files')
|
||||||
flags.DEFINE_integer('public_vlan', 1, 'VLAN for public IP addresses')
|
flags.DEFINE_integer('public_vlan', 1, 'VLAN for public IP addresses')
|
||||||
flags.DEFINE_string('public_interface', 'vlan1', 'Interface for public IP addresses')
|
flags.DEFINE_string('public_interface', 'vlan1',
|
||||||
|
'Interface for public IP addresses')
|
||||||
flags.DEFINE_string('bridge_dev', 'eth1',
|
flags.DEFINE_string('bridge_dev', 'eth1',
|
||||||
'network device for bridges')
|
'network device for bridges')
|
||||||
flags.DEFINE_integer('vlan_start', 100, 'First VLAN for private networks')
|
flags.DEFINE_integer('vlan_start', 100, 'First VLAN for private networks')
|
||||||
flags.DEFINE_integer('vlan_end', 4093, 'Last VLAN for private networks')
|
flags.DEFINE_integer('vlan_end', 4093, 'Last VLAN for private networks')
|
||||||
flags.DEFINE_integer('network_size', 256, 'Number of addresses in each private subnet')
|
flags.DEFINE_integer('network_size', 256,
|
||||||
|
'Number of addresses in each private subnet')
|
||||||
flags.DEFINE_string('public_range', '4.4.4.0/24', 'Public IP address block')
|
flags.DEFINE_string('public_range', '4.4.4.0/24', 'Public IP address block')
|
||||||
flags.DEFINE_string('private_range', '10.0.0.0/8', 'Private IP address block')
|
flags.DEFINE_string('private_range', '10.0.0.0/8', 'Private IP address block')
|
||||||
|
|
||||||
|
|
||||||
# HACK(vish): to delay _get_keeper() loading
|
|
||||||
def _get_keeper():
|
|
||||||
if _get_keeper.keeper == None:
|
|
||||||
_get_keeper.keeper = datastore.Keeper(prefix="net")
|
|
||||||
return _get_keeper.keeper
|
|
||||||
_get_keeper.keeper = None
|
|
||||||
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# CLEANUP:
|
|
||||||
# TODO(ja): use singleton for usermanager instead of self.manager in vlanpool et al
|
|
||||||
# TODO(ja): does vlanpool "keeper" need to know the min/max - shouldn't FLAGS always win?
|
|
||||||
|
|
||||||
class Network(object):
|
class BaseNetwork(datastore.RedisModel):
|
||||||
def __init__(self, *args, **kwargs):
|
bridge_gets_ip = False
|
||||||
self.bridge_gets_ip = False
|
object_type = 'network'
|
||||||
try:
|
|
||||||
os.makedirs(FLAGS.networks_path)
|
|
||||||
except Exception, err:
|
|
||||||
pass
|
|
||||||
self.load(**kwargs)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {'vlan': self.vlan,
|
|
||||||
'network': self.network_str,
|
|
||||||
'hosts': self.hosts}
|
|
||||||
|
|
||||||
def load(self, **kwargs):
|
|
||||||
self.network_str = kwargs.get('network', "192.168.100.0/24")
|
|
||||||
self.hosts = kwargs.get('hosts', {})
|
|
||||||
self.vlan = kwargs.get('vlan', 100)
|
|
||||||
self.name = "nova-%s" % (self.vlan)
|
|
||||||
self.network = IPy.IP(self.network_str)
|
|
||||||
self.gateway = self.network[1]
|
|
||||||
self.netmask = self.network.netmask()
|
|
||||||
self.broadcast = self.network.broadcast()
|
|
||||||
self.bridge_name = "br%s" % (self.vlan)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return json.dumps(self.to_dict())
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return json.dumps(self.to_dict())
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, args):
|
def get_all_hosts(cls):
|
||||||
for arg in args.keys():
|
for vlan in get_assigned_vlans().values():
|
||||||
value = args[arg]
|
network_str = get_subnet_from_vlan(vlan)
|
||||||
del args[arg]
|
for addr in datastore.Redis.instance().hgetall(
|
||||||
args[str(arg)] = value
|
"network:%s:hosts" % (network_str)):
|
||||||
self = cls(**args)
|
yield addr
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, json_string):
|
def create(cls, user_id, project_id, security_group, vlan, network_str):
|
||||||
parsed = json.loads(json_string)
|
network_id = "%s:%s" % (project_id, security_group)
|
||||||
return cls.from_dict(parsed)
|
net = cls(network_id, network_str)
|
||||||
|
net['user_id'] = user_id
|
||||||
|
net['project_id'] = project_id
|
||||||
|
net["vlan"] = vlan
|
||||||
|
net["bridge_name"] = "br%s" % vlan
|
||||||
|
net.save()
|
||||||
|
return net
|
||||||
|
|
||||||
def range(self):
|
def __init__(self, network_id, network_str=None):
|
||||||
for idx in range(3, len(self.network)-2):
|
super(BaseNetwork, self).__init__(object_id=network_id)
|
||||||
yield self.network[idx]
|
self['network_id'] = network_id
|
||||||
|
self['network_str'] = network_str
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network(self):
|
||||||
|
return IPy.IP(self['network_str'])
|
||||||
|
@property
|
||||||
|
def netmask(self):
|
||||||
|
return self.network.netmask()
|
||||||
|
@property
|
||||||
|
def broadcast(self):
|
||||||
|
return self.network.broadcast()
|
||||||
|
@property
|
||||||
|
def bridge_name(self):
|
||||||
|
return "br%s" % (self["vlan"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self):
|
||||||
|
return users.UserManager.instance().get_user(self['user_id'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def project(self):
|
||||||
|
return users.UserManager.instance().get_project(self['project_id'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _hosts_key(self):
|
||||||
|
return "network:%s:hosts" % (self['network_str'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hosts(self):
|
||||||
|
return datastore.Redis.instance().hgetall(self._hosts_key)
|
||||||
|
|
||||||
|
def _add_host(self, _user_id, _project_id, host, target):
|
||||||
|
datastore.Redis.instance().hset(self._hosts_key, host, target)
|
||||||
|
|
||||||
|
def _rem_host(self, host):
|
||||||
|
datastore.Redis.instance().hdel(self._hosts_key, host)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def assigned(self):
|
||||||
|
return datastore.Redis.instance().hkeys(self._hosts_key)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
for idx in range(3, len(self.network) - 1):
|
||||||
|
address = str(self.network[idx])
|
||||||
|
if not address in self.hosts.keys():
|
||||||
|
yield str(address)
|
||||||
|
|
||||||
def allocate_ip(self, user_id, project_id, mac):
|
def allocate_ip(self, user_id, project_id, mac):
|
||||||
for ip in self.range():
|
for address in self.available:
|
||||||
address = str(ip)
|
|
||||||
if not address in self.hosts.keys():
|
|
||||||
logging.debug("Allocating IP %s to %s" % (address, project_id))
|
logging.debug("Allocating IP %s to %s" % (address, project_id))
|
||||||
self.hosts[address] = {
|
self._add_host(user_id, project_id, address, mac)
|
||||||
"address" : address, "user_id": user_id, "project_id" : project_id, 'mac' : mac
|
|
||||||
}
|
|
||||||
self.express(address=address)
|
self.express(address=address)
|
||||||
return address
|
return address
|
||||||
raise exception.NoMoreAddresses()
|
raise exception.NoMoreAddresses()
|
||||||
|
|
||||||
def deallocate_ip(self, ip_str):
|
def deallocate_ip(self, ip_str):
|
||||||
if not ip_str in self.hosts.keys():
|
if not ip_str in self.assigned:
|
||||||
raise exception.AddressNotAllocated()
|
raise exception.AddressNotAllocated()
|
||||||
del self.hosts[ip_str]
|
self._rem_host(ip_str)
|
||||||
# TODO(joshua) SCRUB from the leases file somehow
|
|
||||||
self.deexpress(address=ip_str)
|
self.deexpress(address=ip_str)
|
||||||
|
|
||||||
def list_addresses(self):
|
def list_addresses(self):
|
||||||
for address in self.hosts.values():
|
for address in self.hosts:
|
||||||
yield address
|
yield address
|
||||||
|
|
||||||
def express(self, address=None):
|
def express(self, address=None): pass
|
||||||
pass
|
def deexpress(self, address=None): pass
|
||||||
|
|
||||||
def deexpress(self, address=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Vlan(Network):
|
class BridgedNetwork(BaseNetwork):
|
||||||
"""
|
"""
|
||||||
VLAN configuration, that when expressed creates the vlan
|
Virtual Network that can express itself to create a vlan and
|
||||||
|
a bridge (with or without an IP address/netmask/gateway)
|
||||||
properties:
|
|
||||||
|
|
||||||
vlan - integer (example: 42)
|
|
||||||
bridge_dev - string (example: eth0)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(Vlan, self).__init__(*args, **kwargs)
|
|
||||||
self.bridge_dev = FLAGS.bridge_dev
|
|
||||||
|
|
||||||
def express(self, address=None):
|
|
||||||
super(Vlan, self).express(address=address)
|
|
||||||
try:
|
|
||||||
logging.debug("Starting VLAN inteface for %s network" % (self.vlan))
|
|
||||||
linux_net.vlan_create(self)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VirtNetwork(Vlan):
|
|
||||||
"""
|
|
||||||
Virtual Network that can export libvirt configuration or express itself to
|
|
||||||
create a bridge (with or without an IP address/netmask/gateway)
|
|
||||||
|
|
||||||
properties:
|
properties:
|
||||||
bridge_name - string (example value: br42)
|
bridge_name - string (example value: br42)
|
||||||
vlan - integer (example value: 42)
|
vlan - integer (example value: 42)
|
||||||
|
bridge_dev - string (example: eth0)
|
||||||
bridge_gets_ip - boolean used during bridge creation
|
bridge_gets_ip - boolean used during bridge creation
|
||||||
|
|
||||||
if bridge_gets_ip then network address for bridge uses the properties:
|
if bridge_gets_ip then network address for bridge uses the properties:
|
||||||
@@ -181,333 +166,242 @@ class VirtNetwork(Vlan):
|
|||||||
netmask
|
netmask
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_network_for_project(cls, user_id, project_id, security_group):
|
||||||
|
vlan = get_vlan_for_project(project_id)
|
||||||
|
network_str = get_subnet_from_vlan(vlan)
|
||||||
|
logging.debug("creating network on vlan %s with network string %s" % (vlan, network_str))
|
||||||
|
return cls.create(user_id, project_id, security_group, vlan, network_str)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(VirtNetwork, self).__init__(*args, **kwargs)
|
super(BridgedNetwork, self).__init__(*args, **kwargs)
|
||||||
|
self['bridge_dev'] = FLAGS.bridge_dev
|
||||||
def virtXML(self):
|
self.save()
|
||||||
""" generate XML for libvirt network """
|
|
||||||
|
|
||||||
libvirt_xml = open(FLAGS.net_libvirt_xml_template).read()
|
|
||||||
xml_info = {'name' : self.name,
|
|
||||||
'bridge_name' : self.bridge_name,
|
|
||||||
'device' : "vlan%s" % (self.vlan),
|
|
||||||
'gateway' : self.gateway,
|
|
||||||
'netmask' : self.netmask,
|
|
||||||
}
|
|
||||||
libvirt_xml = libvirt_xml % xml_info
|
|
||||||
return libvirt_xml
|
|
||||||
|
|
||||||
def express(self, address=None):
|
def express(self, address=None):
|
||||||
""" creates a bridge device on top of the Vlan """
|
super(BridgedNetwork, self).express(address=address)
|
||||||
super(VirtNetwork, self).express(address=address)
|
linux_net.vlan_create(self)
|
||||||
try:
|
|
||||||
logging.debug("Starting Bridge inteface for %s network" % (self.vlan))
|
|
||||||
linux_net.bridge_create(self)
|
linux_net.bridge_create(self)
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class DHCPNetwork(VirtNetwork):
|
class DHCPNetwork(BridgedNetwork):
|
||||||
"""
|
"""
|
||||||
properties:
|
properties:
|
||||||
dhcp_listen_address: the ip of the gateway / dhcp host
|
dhcp_listen_address: the ip of the gateway / dhcp host
|
||||||
dhcp_range_start: the first ip to give out
|
dhcp_range_start: the first ip to give out
|
||||||
dhcp_range_end: the last ip to give out
|
dhcp_range_end: the last ip to give out
|
||||||
"""
|
"""
|
||||||
|
bridge_gets_ip = True
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DHCPNetwork, self).__init__(*args, **kwargs)
|
super(DHCPNetwork, self).__init__(*args, **kwargs)
|
||||||
logging.debug("Initing DHCPNetwork object...")
|
logging.debug("Initing DHCPNetwork object...")
|
||||||
self.bridge_gets_ip = True
|
|
||||||
self.dhcp_listen_address = self.network[1]
|
self.dhcp_listen_address = self.network[1]
|
||||||
self.dhcp_range_start = self.network[3]
|
self.dhcp_range_start = self.network[3]
|
||||||
self.dhcp_range_end = self.network[-2]
|
self.dhcp_range_end = self.network[-(1 + FLAGS.cnt_vpn_clients)]
|
||||||
|
try:
|
||||||
|
os.makedirs(FLAGS.networks_path)
|
||||||
|
except Exception, err:
|
||||||
|
pass
|
||||||
|
|
||||||
def express(self, address=None):
|
def express(self, address=None):
|
||||||
super(DHCPNetwork, self).express(address=address)
|
super(DHCPNetwork, self).express(address=address)
|
||||||
if len(self.hosts.values()) > 0:
|
if len(self.assigned) > 0:
|
||||||
logging.debug("Starting dnsmasq server for network with vlan %s" % self.vlan)
|
logging.debug("Starting dnsmasq server for network with vlan %s",
|
||||||
|
self['vlan'])
|
||||||
linux_net.start_dnsmasq(self)
|
linux_net.start_dnsmasq(self)
|
||||||
else:
|
else:
|
||||||
logging.debug("Not launching dnsmasq cause I don't think we have any hosts.")
|
logging.debug("Not launching dnsmasq: no hosts.")
|
||||||
|
|
||||||
def deexpress(self, address=None):
|
def deexpress(self, address=None):
|
||||||
# if this is the last address, stop dns
|
# if this is the last address, stop dns
|
||||||
super(DHCPNetwork, self).deexpress(address=address)
|
super(DHCPNetwork, self).deexpress(address=address)
|
||||||
if len(self.hosts.values()) == 0:
|
if len(self.assigned) == 0:
|
||||||
linux_net.stop_dnsmasq(self)
|
linux_net.stop_dnsmasq(self)
|
||||||
else:
|
else:
|
||||||
linux_net.start_dnsmasq(self)
|
linux_net.start_dnsmasq(self)
|
||||||
|
|
||||||
class PrivateNetwork(DHCPNetwork):
|
class PublicAddress(datastore.RedisModel):
|
||||||
def __init__(self, **kwargs):
|
object_type="address"
|
||||||
super(PrivateNetwork, self).__init__(**kwargs)
|
|
||||||
# self.express()
|
|
||||||
|
|
||||||
def to_dict(self):
|
def __init__(self, address):
|
||||||
return {'vlan': self.vlan,
|
super(PublicAddress, self).__init__(address)
|
||||||
'network': self.network_str,
|
|
||||||
'hosts': self.hosts}
|
|
||||||
|
|
||||||
class PublicNetwork(Network):
|
@classmethod
|
||||||
def __init__(self, network="192.168.216.0/24", **kwargs):
|
def create(cls, user_id, project_id, address):
|
||||||
super(PublicNetwork, self).__init__(network=network, **kwargs)
|
addr = cls(address=address)
|
||||||
|
addr['address'] = address
|
||||||
|
addr['user_id'] = user_id
|
||||||
|
addr['project_id'] = project_id
|
||||||
|
addr['instance_id'] = 'available'
|
||||||
|
addr['private_ip'] = 'available'
|
||||||
|
addr["create_time"] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||||
|
addr.save()
|
||||||
|
return addr
|
||||||
|
|
||||||
|
DEFAULT_PORTS = [("tcp",80), ("tcp",22), ("udp",1194), ("tcp",443)]
|
||||||
|
class PublicNetworkController(BaseNetwork):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
network_id = "public:default"
|
||||||
|
super(PublicNetworkController, self).__init__(network_id, FLAGS.public_range)
|
||||||
|
self['user_id'] = "public"
|
||||||
|
self['project_id'] = "public"
|
||||||
|
self["create_time"] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||||
|
self["vlan"] = FLAGS.public_vlan
|
||||||
|
self.save()
|
||||||
self.express()
|
self.express()
|
||||||
|
|
||||||
def allocate_ip(self, user_id, project_id, mac):
|
@property
|
||||||
for ip in self.range():
|
def available(self):
|
||||||
address = str(ip)
|
for idx in range(2, len(self.network)-1):
|
||||||
|
address = str(self.network[idx])
|
||||||
if not address in self.hosts.keys():
|
if not address in self.hosts.keys():
|
||||||
logging.debug("Allocating IP %s to %s" % (address, project_id))
|
yield address
|
||||||
self.hosts[address] = {
|
|
||||||
"address" : address, "user_id": user_id, "project_id" : project_id, 'mac' : mac
|
|
||||||
}
|
|
||||||
self.express(address=address)
|
|
||||||
return address
|
|
||||||
raise exception.NoMoreAddresses()
|
|
||||||
|
|
||||||
def deallocate_ip(self, ip_str):
|
@property
|
||||||
if not ip_str in self.hosts:
|
def host_objs(self):
|
||||||
raise exception.AddressNotAllocated()
|
for address in self.assigned:
|
||||||
del self.hosts[ip_str]
|
yield PublicAddress(address)
|
||||||
# TODO(joshua) SCRUB from the leases file somehow
|
|
||||||
self.deexpress(address=ip_str)
|
def get_public_ip_for_instance(self, instance_id):
|
||||||
|
# FIXME: this should be a lookup - iteration won't scale
|
||||||
|
for address_record in self.host_objs:
|
||||||
|
if address_record.get('instance_id', 'available') == instance_id:
|
||||||
|
return address_record['address']
|
||||||
|
|
||||||
|
def get_host(self, host):
|
||||||
|
if host in self.assigned:
|
||||||
|
return PublicAddress(host)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _add_host(self, user_id, project_id, host, _target):
|
||||||
|
datastore.Redis.instance().hset(self._hosts_key, host, project_id)
|
||||||
|
PublicAddress.create(user_id, project_id, host)
|
||||||
|
|
||||||
|
def _rem_host(self, host):
|
||||||
|
PublicAddress(host).destroy()
|
||||||
|
datastore.Redis.instance().hdel(self._hosts_key, host)
|
||||||
|
|
||||||
def associate_address(self, public_ip, private_ip, instance_id):
|
def associate_address(self, public_ip, private_ip, instance_id):
|
||||||
if not public_ip in self.hosts:
|
if not public_ip in self.assigned:
|
||||||
raise exception.AddressNotAllocated()
|
raise exception.AddressNotAllocated()
|
||||||
for addr in self.hosts.values():
|
# TODO(joshua): Keep an index going both ways
|
||||||
if addr.has_key('private_ip') and addr['private_ip'] == private_ip:
|
for addr in self.host_objs:
|
||||||
|
if addr.get('private_ip', None) == private_ip:
|
||||||
raise exception.AddressAlreadyAssociated()
|
raise exception.AddressAlreadyAssociated()
|
||||||
if self.hosts[public_ip].has_key('private_ip'):
|
addr = self.get_host(public_ip)
|
||||||
|
if addr.get('private_ip', 'available') != 'available':
|
||||||
raise exception.AddressAlreadyAssociated()
|
raise exception.AddressAlreadyAssociated()
|
||||||
self.hosts[public_ip]['private_ip'] = private_ip
|
addr['private_ip'] = private_ip
|
||||||
self.hosts[public_ip]['instance_id'] = instance_id
|
addr['instance_id'] = instance_id
|
||||||
|
addr.save()
|
||||||
self.express(address=public_ip)
|
self.express(address=public_ip)
|
||||||
|
|
||||||
def disassociate_address(self, public_ip):
|
def disassociate_address(self, public_ip):
|
||||||
if not public_ip in self.hosts:
|
if not public_ip in self.assigned:
|
||||||
raise exception.AddressNotAllocated()
|
raise exception.AddressNotAllocated()
|
||||||
if not self.hosts[public_ip].has_key('private_ip'):
|
addr = self.get_host(public_ip)
|
||||||
|
if addr.get('private_ip', 'available') == 'available':
|
||||||
raise exception.AddressNotAssociated()
|
raise exception.AddressNotAssociated()
|
||||||
self.deexpress(public_ip)
|
self.deexpress(address=public_ip)
|
||||||
del self.hosts[public_ip]['private_ip']
|
addr['private_ip'] = 'available'
|
||||||
del self.hosts[public_ip]['instance_id']
|
addr['instance_id'] = 'available'
|
||||||
# TODO Express the removal
|
addr.save()
|
||||||
|
|
||||||
def deexpress(self, address):
|
|
||||||
addr = self.hosts[address]
|
|
||||||
public_ip = addr['address']
|
|
||||||
private_ip = addr['private_ip']
|
|
||||||
linux_net.remove_rule("PREROUTING -t nat -d %s -j DNAT --to %s" % (public_ip, private_ip))
|
|
||||||
linux_net.remove_rule("POSTROUTING -t nat -s %s -j SNAT --to %s" % (private_ip, public_ip))
|
|
||||||
linux_net.remove_rule("FORWARD -d %s -p icmp -j ACCEPT" % (private_ip))
|
|
||||||
for (protocol, port) in [("tcp",80), ("tcp",22), ("udp",1194), ("tcp",443)]:
|
|
||||||
linux_net.remove_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT" % (private_ip, protocol, port))
|
|
||||||
|
|
||||||
def express(self, address=None):
|
def express(self, address=None):
|
||||||
logging.debug("Todo - need to create IPTables natting entries for this net.")
|
addresses = self.host_objs
|
||||||
addresses = self.hosts.values()
|
|
||||||
if address:
|
if address:
|
||||||
addresses = [self.hosts[address]]
|
addresses = [self.get_host(address)]
|
||||||
for addr in addresses:
|
for addr in addresses:
|
||||||
if not addr.has_key('private_ip'):
|
if addr.get('private_ip','available') == 'available':
|
||||||
continue
|
continue
|
||||||
public_ip = addr['address']
|
public_ip = addr['address']
|
||||||
private_ip = addr['private_ip']
|
private_ip = addr['private_ip']
|
||||||
linux_net.bind_public_ip(public_ip, FLAGS.public_interface)
|
linux_net.bind_public_ip(public_ip, FLAGS.public_interface)
|
||||||
linux_net.confirm_rule("PREROUTING -t nat -d %s -j DNAT --to %s" % (public_ip, private_ip))
|
linux_net.confirm_rule("PREROUTING -t nat -d %s -j DNAT --to %s"
|
||||||
linux_net.confirm_rule("POSTROUTING -t nat -s %s -j SNAT --to %s" % (private_ip, public_ip))
|
% (public_ip, private_ip))
|
||||||
|
linux_net.confirm_rule("POSTROUTING -t nat -s %s -j SNAT --to %s"
|
||||||
|
% (private_ip, public_ip))
|
||||||
# TODO: Get these from the secgroup datastore entries
|
# TODO: Get these from the secgroup datastore entries
|
||||||
linux_net.confirm_rule("FORWARD -d %s -p icmp -j ACCEPT" % (private_ip))
|
linux_net.confirm_rule("FORWARD -d %s -p icmp -j ACCEPT"
|
||||||
for (protocol, port) in [("tcp",80), ("tcp",22), ("udp",1194), ("tcp",443)]:
|
% (private_ip))
|
||||||
linux_net.confirm_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT" % (private_ip, protocol, port))
|
for (protocol, port) in DEFAULT_PORTS:
|
||||||
|
linux_net.confirm_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT"
|
||||||
|
% (private_ip, protocol, port))
|
||||||
|
|
||||||
|
def deexpress(self, address=None):
|
||||||
|
addr = self.get_host(address)
|
||||||
|
private_ip = addr['private_ip']
|
||||||
|
linux_net.remove_rule("PREROUTING -t nat -d %s -j DNAT --to %s"
|
||||||
|
% (address, private_ip))
|
||||||
|
linux_net.remove_rule("POSTROUTING -t nat -s %s -j SNAT --to %s"
|
||||||
|
% (private_ip, address))
|
||||||
|
linux_net.remove_rule("FORWARD -d %s -p icmp -j ACCEPT"
|
||||||
|
% (private_ip))
|
||||||
|
for (protocol, port) in DEFAULT_PORTS:
|
||||||
|
linux_net.remove_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT"
|
||||||
|
% (private_ip, protocol, port))
|
||||||
|
|
||||||
|
|
||||||
class NetworkPool(object):
|
VLANS_KEY = "vlans"
|
||||||
# TODO - Allocations need to be system global
|
def _add_vlan(project_id, vlan):
|
||||||
|
datastore.Redis.instance().hset(VLANS_KEY, project_id, vlan)
|
||||||
|
|
||||||
def __init__(self):
|
def _rem_vlan(project_id):
|
||||||
self.network = IPy.IP(FLAGS.private_range)
|
datastore.Redis.instance().hdel(VLANS_KEY, project_id)
|
||||||
netsize = FLAGS.network_size
|
|
||||||
if not netsize in [4,8,16,32,64,128,256,512,1024]:
|
|
||||||
raise exception.NotValidNetworkSize()
|
|
||||||
self.netsize = netsize
|
|
||||||
self.startvlan = FLAGS.vlan_start
|
|
||||||
|
|
||||||
def get_from_vlan(self, vlan):
|
def get_assigned_vlans():
|
||||||
start = (vlan-self.startvlan) * self.netsize
|
""" Returns a dictionary, with keys of project_id and values of vlan_id """
|
||||||
net_str = "%s-%s" % (self.network[start], self.network[start + self.netsize - 1])
|
return datastore.Redis.instance().hgetall(VLANS_KEY)
|
||||||
logging.debug("Allocating %s" % net_str)
|
|
||||||
return net_str
|
|
||||||
|
|
||||||
|
def get_vlan_for_project(project_id):
|
||||||
class VlanPool(object):
|
"""
|
||||||
def __init__(self, **kwargs):
|
Allocate vlan IDs to individual users.
|
||||||
self.start = FLAGS.vlan_start
|
"""
|
||||||
self.end = FLAGS.vlan_end
|
vlan = datastore.Redis.instance().hget(VLANS_KEY, project_id)
|
||||||
self.vlans = kwargs.get('vlans', {})
|
if vlan:
|
||||||
self.vlanpool = {}
|
return vlan
|
||||||
self.manager = users.UserManager.instance()
|
assigned_vlans = get_assigned_vlans()
|
||||||
for project_id, vlan in self.vlans.iteritems():
|
# TODO(joshua) I can do this in one loop, I think
|
||||||
self.vlanpool[vlan] = project_id
|
for old_project_id, vlan in assigned_vlans.iteritems():
|
||||||
|
if not users.UserManager.instance().get_project(old_project_id):
|
||||||
def to_dict(self):
|
_rem_vlan(old_project_id)
|
||||||
return {'vlans': self.vlans}
|
_add_vlan(project_id, vlan)
|
||||||
|
return vlan
|
||||||
def __str__(self):
|
for vlan in range(FLAGS.vlan_start, FLAGS.vlan_end):
|
||||||
return json.dumps(self.to_dict())
|
if not str(vlan) in assigned_vlans.values():
|
||||||
|
_add_vlan(project_id, vlan)
|
||||||
def __unicode__(self):
|
return vlan
|
||||||
return json.dumps(self.to_dict())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, args):
|
|
||||||
for arg in args.keys():
|
|
||||||
value = args[arg]
|
|
||||||
del args[arg]
|
|
||||||
args[str(arg)] = value
|
|
||||||
self = cls(**args)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_json(cls, json_string):
|
|
||||||
parsed = json.loads(json_string)
|
|
||||||
return cls.from_dict(parsed)
|
|
||||||
|
|
||||||
def assign_vlan(self, project_id, vlan):
|
|
||||||
logging.debug("Assigning vlan %s to project %s" % (vlan, project_id))
|
|
||||||
self.vlans[project_id] = vlan
|
|
||||||
self.vlanpool[vlan] = project_id
|
|
||||||
return self.vlans[project_id]
|
|
||||||
|
|
||||||
def next(self, project_id):
|
|
||||||
for old_project_id, vlan in self.vlans.iteritems():
|
|
||||||
if not self.manager.get_project(old_project_id):
|
|
||||||
_get_keeper()["%s-default" % old_project_id] = {}
|
|
||||||
del _get_keeper()["%s-default" % old_project_id]
|
|
||||||
del self.vlans[old_project_id]
|
|
||||||
return self.assign_vlan(project_id, vlan)
|
|
||||||
vlans = self.vlanpool.keys()
|
|
||||||
vlans.append(self.start)
|
|
||||||
nextvlan = max(vlans) + 1
|
|
||||||
if nextvlan == self.end:
|
|
||||||
raise exception.AddressNotAllocated("Out of VLANs")
|
raise exception.AddressNotAllocated("Out of VLANs")
|
||||||
return self.assign_vlan(project_id, nextvlan)
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkController(object):
|
def get_network_by_address(address):
|
||||||
""" The network controller is in charge of network connections """
|
for project in users.UserManager.instance().get_projects():
|
||||||
|
net = get_project_network(project.id)
|
||||||
def __init__(self, **kwargs):
|
if address in net.assigned:
|
||||||
logging.debug("Starting up the network controller.")
|
return net
|
||||||
self.manager = users.UserManager.instance()
|
|
||||||
self._pubnet = None
|
|
||||||
if not _get_keeper()['vlans']:
|
|
||||||
_get_keeper()['vlans'] = {}
|
|
||||||
if not _get_keeper()['public']:
|
|
||||||
_get_keeper()['public'] = {'vlan': FLAGS.public_vlan, 'network' : FLAGS.public_range}
|
|
||||||
self.express()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
_get_keeper()['public'] = {'vlan': FLAGS.public_vlan, 'network': FLAGS.public_range }
|
|
||||||
_get_keeper()['vlans'] = {}
|
|
||||||
# TODO : Get rid of old interfaces, bridges, and IPTables rules.
|
|
||||||
|
|
||||||
@property
|
|
||||||
def public_net(self):
|
|
||||||
if not self._pubnet:
|
|
||||||
self._pubnet = PublicNetwork.from_dict(_get_keeper()['public'])
|
|
||||||
self._pubnet.load(**_get_keeper()['public'])
|
|
||||||
return self._pubnet
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vlan_pool(self):
|
|
||||||
return VlanPool.from_dict(_get_keeper()['vlans'])
|
|
||||||
|
|
||||||
def get_network_from_name(self, network_name):
|
|
||||||
net_dict = _get_keeper()[network_name]
|
|
||||||
if net_dict:
|
|
||||||
return PrivateNetwork.from_dict(net_dict)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_public_ip_for_instance(self, instance_id):
|
|
||||||
# FIXME: this should be a lookup - iteration won't scale
|
|
||||||
for address_record in self.describe_addresses(type=PublicNetwork):
|
|
||||||
if address_record.get(u'instance_id', 'free') == instance_id:
|
|
||||||
return address_record[u'address']
|
|
||||||
|
|
||||||
def get_project_network(self, project_id):
|
|
||||||
""" get a project's private network, allocating one if needed """
|
|
||||||
|
|
||||||
project = self.manager.get_project(project_id)
|
|
||||||
if not project:
|
|
||||||
raise Exception("Project %s doesn't exist, uhoh." % project_id)
|
|
||||||
project_net = self.get_network_from_name("%s-default" % project_id)
|
|
||||||
if not project_net:
|
|
||||||
pool = self.vlan_pool
|
|
||||||
vlan = pool.next(project_id)
|
|
||||||
private_pool = NetworkPool()
|
|
||||||
network_str = private_pool.get_from_vlan(vlan)
|
|
||||||
logging.debug("Constructing network %s and %s for %s" % (network_str, vlan, project_id))
|
|
||||||
project_net = PrivateNetwork(
|
|
||||||
network=network_str,
|
|
||||||
vlan=vlan)
|
|
||||||
_get_keeper()["%s-default" % project_id] = project_net.to_dict()
|
|
||||||
_get_keeper()['vlans'] = pool.to_dict()
|
|
||||||
return project_net
|
|
||||||
|
|
||||||
def allocate_address(self, user_id, project_id, mac=None, type=PrivateNetwork):
|
|
||||||
ip = None
|
|
||||||
net_name = None
|
|
||||||
if type == PrivateNetwork:
|
|
||||||
net = self.get_project_network(project_id)
|
|
||||||
ip = net.allocate_ip(user_id, project_id, mac)
|
|
||||||
net_name = net.name
|
|
||||||
_get_keeper()["%s-default" % project_id] = net.to_dict()
|
|
||||||
else:
|
|
||||||
net = self.public_net
|
|
||||||
ip = net.allocate_ip(user_id, project_id, mac)
|
|
||||||
net_name = net.name
|
|
||||||
_get_keeper()['public'] = net.to_dict()
|
|
||||||
return (ip, net_name)
|
|
||||||
|
|
||||||
def deallocate_address(self, address):
|
|
||||||
if address in self.public_net.network:
|
|
||||||
net = self.public_net
|
|
||||||
rv = net.deallocate_ip(str(address))
|
|
||||||
_get_keeper()['public'] = net.to_dict()
|
|
||||||
return rv
|
|
||||||
for project in self.manager.get_projects():
|
|
||||||
if address in self.get_project_network(project.id).network:
|
|
||||||
net = self.get_project_network(project.id)
|
|
||||||
rv = net.deallocate_ip(str(address))
|
|
||||||
_get_keeper()["%s-default" % project.id] = net.to_dict()
|
|
||||||
return rv
|
|
||||||
raise exception.AddressNotAllocated()
|
raise exception.AddressNotAllocated()
|
||||||
|
|
||||||
def describe_addresses(self, type=PrivateNetwork):
|
def allocate_ip(user_id, project_id, mac):
|
||||||
if type == PrivateNetwork:
|
return get_project_network(project_id).allocate_ip(user_id, project_id, mac)
|
||||||
addresses = []
|
|
||||||
for project in self.manager.get_projects():
|
|
||||||
addresses.extend(self.get_project_network(project.id).list_addresses())
|
|
||||||
return addresses
|
|
||||||
return self.public_net.list_addresses()
|
|
||||||
|
|
||||||
def associate_address(self, address, private_ip, instance_id):
|
def deallocate_ip(address):
|
||||||
net = self.public_net
|
return get_network_by_address(address).deallocate_ip(address)
|
||||||
rv = net.associate_address(address, private_ip, instance_id)
|
|
||||||
_get_keeper()['public'] = net.to_dict()
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def disassociate_address(self, address):
|
def get_project_network(project_id, security_group='default'):
|
||||||
net = self.public_net
|
""" get a project's private network, allocating one if needed """
|
||||||
rv = net.disassociate_address(address)
|
project = users.UserManager.instance().get_project(project_id)
|
||||||
_get_keeper()['public'] = net.to_dict()
|
if not project:
|
||||||
return rv
|
raise nova.exception.Error("Project %s doesn't exist, uhoh." % project_id)
|
||||||
|
return DHCPNetwork.get_network_for_project(project.project_manager_id, project.id, security_group)
|
||||||
|
|
||||||
def express(self,address=None):
|
def get_subnet_from_vlan(vlan):
|
||||||
for project in self.manager.get_projects():
|
"""Assign one subnet to each VLAN, for now."""
|
||||||
self.get_project_network(project.id).express()
|
vlan = int(vlan)
|
||||||
|
network = IPy.IP(FLAGS.private_range)
|
||||||
def report_state(self):
|
start = (vlan-FLAGS.vlan_start) * FLAGS.network_size
|
||||||
pass
|
return "%s-%s" % (network[start], network[start + FLAGS.network_size - 1])
|
||||||
|
|
||||||
|
def restart_nets():
|
||||||
|
""" Ensure the network for each user is enabled"""
|
||||||
|
for project in users.UserManager.instance().get_projects():
|
||||||
|
get_project_network(project.id).express()
|
||||||
|
|||||||
@@ -137,11 +137,15 @@ class Node(object, service.Service):
|
|||||||
logging.debug("Reporting State")
|
logging.debug("Reporting State")
|
||||||
return
|
return
|
||||||
|
|
||||||
@exception.wrap_exception
|
# @exception.wrap_exception
|
||||||
def run_instance(self, instance_id, **_kwargs):
|
def run_instance(self, instance_id, **_kwargs):
|
||||||
""" launch a new instance with specified options """
|
""" launch a new instance with specified options """
|
||||||
logging.debug("Starting instance %s..." % (instance_id))
|
logging.debug("Starting instance %s..." % (instance_id))
|
||||||
inst = self.instdir.get(instance_id)
|
inst = self.instdir.get(instance_id)
|
||||||
|
# TODO: Get the real security group of launch in here
|
||||||
|
security_group = "default"
|
||||||
|
net = network.BridgedNetwork.get_network_for_project(inst['user_id'], inst['project_id'],
|
||||||
|
security_group).express()
|
||||||
inst['node_name'] = FLAGS.node_name
|
inst['node_name'] = FLAGS.node_name
|
||||||
inst.save()
|
inst.save()
|
||||||
# TODO(vish) check to make sure the availability zone matches
|
# TODO(vish) check to make sure the availability zone matches
|
||||||
@@ -337,8 +341,6 @@ class Instance(object):
|
|||||||
'basepath', os.path.abspath(
|
'basepath', os.path.abspath(
|
||||||
os.path.join(FLAGS.instances_path, self.name)))
|
os.path.join(FLAGS.instances_path, self.name)))
|
||||||
self._s['memory_kb'] = int(self._s['memory_mb']) * 1024
|
self._s['memory_kb'] = int(self._s['memory_mb']) * 1024
|
||||||
# TODO(joshua) - Get this from network directory controller later
|
|
||||||
self._s['bridge_name'] = data.get('bridge_name', 'br0')
|
|
||||||
self._s['image_id'] = data.get('image_id', FLAGS.default_image)
|
self._s['image_id'] = data.get('image_id', FLAGS.default_image)
|
||||||
self._s['kernel_id'] = data.get('kernel_id', FLAGS.default_kernel)
|
self._s['kernel_id'] = data.get('kernel_id', FLAGS.default_kernel)
|
||||||
self._s['ramdisk_id'] = data.get('ramdisk_id', FLAGS.default_ramdisk)
|
self._s['ramdisk_id'] = data.get('ramdisk_id', FLAGS.default_ramdisk)
|
||||||
@@ -360,6 +362,7 @@ class Instance(object):
|
|||||||
self._s['addressing_type'] = data.get('addressing_type', None)
|
self._s['addressing_type'] = data.get('addressing_type', None)
|
||||||
self._s['availability_zone'] = data.get('availability_zone', 'fixme')
|
self._s['availability_zone'] = data.get('availability_zone', 'fixme')
|
||||||
|
|
||||||
|
self._s['bridge_name'] = data.get('bridge_name', None)
|
||||||
#TODO: put real dns items here
|
#TODO: put real dns items here
|
||||||
self._s['private_dns_name'] = data.get('private_dns_name', 'fixme')
|
self._s['private_dns_name'] = data.get('private_dns_name', 'fixme')
|
||||||
self._s['dns_name'] = data.get('dns_name',
|
self._s['dns_name'] = data.get('dns_name',
|
||||||
@@ -476,7 +479,7 @@ class Instance(object):
|
|||||||
logging.debug('rebooted instance %s' % self.name)
|
logging.debug('rebooted instance %s' % self.name)
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
|
|
||||||
@exception.wrap_exception
|
# @exception.wrap_exception
|
||||||
def spawn(self):
|
def spawn(self):
|
||||||
self.datamodel['state'] = "spawning"
|
self.datamodel['state'] = "spawning"
|
||||||
self.datamodel.save()
|
self.datamodel.save()
|
||||||
@@ -516,30 +519,3 @@ class Instance(object):
|
|||||||
else:
|
else:
|
||||||
console = 'FAKE CONSOLE OUTPUT'
|
console = 'FAKE CONSOLE OUTPUT'
|
||||||
return defer.succeed(console)
|
return defer.succeed(console)
|
||||||
|
|
||||||
def generate_mac(self):
|
|
||||||
mac = [0x00, 0x16, 0x3e, random.randint(0x00, 0x7f),
|
|
||||||
random.randint(0x00, 0xff), random.randint(0x00, 0xff)
|
|
||||||
]
|
|
||||||
return ':'.join(map(lambda x: "%02x" % x, mac))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkNode(Node):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super(NetworkNode, self).__init__(**kwargs)
|
|
||||||
self.virtNets = {}
|
|
||||||
|
|
||||||
def add_network(self, net_dict):
|
|
||||||
net = network.VirtNetwork(**net_dict)
|
|
||||||
self.virtNets[net.name] = net
|
|
||||||
self.virtNets[net.name].express()
|
|
||||||
return defer.succeed({'retval': 'network added'})
|
|
||||||
|
|
||||||
@exception.wrap_exception
|
|
||||||
def run_instance(self, instance_id, **kwargs):
|
|
||||||
inst = self.instdir.get(instance_id)
|
|
||||||
net_dict = json.loads(inst.get('network_str', "{}"))
|
|
||||||
self.add_network(net_dict)
|
|
||||||
return super(NetworkNode, self).run_instance(instance_id, **kwargs)
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
from nova import vendor
|
from nova import vendor
|
||||||
import redis
|
import redis
|
||||||
@@ -77,10 +78,11 @@ class RedisModel(object):
|
|||||||
def set_default_state(self):
|
def set_default_state(self):
|
||||||
self.state = {'state' : 'pending'}
|
self.state = {'state' : 'pending'}
|
||||||
self.state[self.object_type+"_id"] = self.object_id
|
self.state[self.object_type+"_id"] = self.object_id
|
||||||
|
self.state["create_time"] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __redis_key(self):
|
def __redis_key(self):
|
||||||
""" Magic string for instance keys """
|
""" Magic string for keys """
|
||||||
return '%s:%s' % (self.object_type, self.object_id)
|
return '%s:%s' % (self.object_type, self.object_id)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class CloudController(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.instdir = model.InstanceDirectory()
|
self.instdir = model.InstanceDirectory()
|
||||||
self.network = network.NetworkController()
|
self.network = network.PublicNetworkController()
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -254,21 +254,10 @@ class CloudController(object):
|
|||||||
res.addCallback(_format_result)
|
res.addCallback(_format_result)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _convert_address(self, network_address):
|
|
||||||
# FIXME(vish): this should go away when network.py stores info properly
|
|
||||||
address = {}
|
|
||||||
address['public_ip'] == network_address[u'address']
|
|
||||||
address['user_id'] == network_address[u'user_id']
|
|
||||||
address['project_id'] == network_address.get(u'project_id', address['user_id'])
|
|
||||||
address['instance_id'] == network_address.get(u'instance_id', None)
|
|
||||||
return address
|
|
||||||
|
|
||||||
def _get_address(self, context, public_ip):
|
def _get_address(self, context, public_ip):
|
||||||
# right now all addresses are allocated locally
|
|
||||||
# FIXME(vish) this should move into network.py
|
# FIXME(vish) this should move into network.py
|
||||||
for network_address in self.network.describe_addresses():
|
for address in self.network.hosts:
|
||||||
if network_address[u'address'] == public_ip:
|
if address['address'] == public_ip:
|
||||||
address = self._convert_address(network_address)
|
|
||||||
if context.user.is_admin() or address['project_id'] == context.project.id:
|
if context.user.is_admin() or address['project_id'] == context.project.id:
|
||||||
return address
|
return address
|
||||||
raise exception.NotFound("Address at ip %s not found" % public_ip)
|
raise exception.NotFound("Address at ip %s not found" % public_ip)
|
||||||
@@ -398,14 +387,12 @@ class CloudController(object):
|
|||||||
def format_addresses(self, context):
|
def format_addresses(self, context):
|
||||||
addresses = []
|
addresses = []
|
||||||
# TODO(vish): move authorization checking into network.py
|
# TODO(vish): move authorization checking into network.py
|
||||||
for network_address in self.network.describe_addresses(type=network.PublicNetwork):
|
for address in self.network.hosts:
|
||||||
#logging.debug(address_record)
|
#logging.debug(address_record)
|
||||||
address = self._convert_address(network_address)
|
|
||||||
address_rv = {
|
address_rv = {
|
||||||
'public_ip': address['public_ip'],
|
'public_ip': address['address'],
|
||||||
'instance_id' : address.get('instance_id', 'free')
|
'instance_id' : address.get('instance_id', 'free')
|
||||||
}
|
}
|
||||||
# FIXME: add another field for user id
|
|
||||||
if context.user.is_admin():
|
if context.user.is_admin():
|
||||||
address_rv['instance_id'] = "%s (%s, %s)" % (
|
address_rv['instance_id'] = "%s (%s, %s)" % (
|
||||||
address['instance_id'],
|
address['instance_id'],
|
||||||
@@ -417,17 +404,17 @@ class CloudController(object):
|
|||||||
return {'addressesSet': addresses}
|
return {'addressesSet': addresses}
|
||||||
|
|
||||||
def allocate_address(self, context, **kwargs):
|
def allocate_address(self, context, **kwargs):
|
||||||
(address,network_name) = self.network.allocate_address(
|
address = self.network.allocate_ip(
|
||||||
context.user.id, context.project_id, type=network.PublicNetwork)
|
context.user.id, context.project.id, 'public')
|
||||||
return defer.succeed({'addressSet': [{'publicIp' : address}]})
|
return defer.succeed({'addressSet': [{'publicIp' : address}]})
|
||||||
|
|
||||||
def release_address(self, context, public_ip, **kwargs):
|
def release_address(self, context, public_ip, **kwargs):
|
||||||
address = self._get_address(public_ip)
|
self.network.deallocate_ip(public_ip)
|
||||||
return defer.succeed({'releaseResponse': ["Address released."]})
|
return defer.succeed({'releaseResponse': ["Address released."]})
|
||||||
|
|
||||||
def associate_address(self, context, instance_id, **kwargs):
|
def associate_address(self, context, instance_id, **kwargs):
|
||||||
instance = self._get_instance(context, instance_id)
|
instance = self._get_instance(context, instance_id)
|
||||||
rv = self.network.associate_address(
|
self.network.associate_address(
|
||||||
kwargs['public_ip'],
|
kwargs['public_ip'],
|
||||||
instance['private_dns_name'],
|
instance['private_dns_name'],
|
||||||
instance_id)
|
instance_id)
|
||||||
@@ -435,7 +422,7 @@ class CloudController(object):
|
|||||||
|
|
||||||
def disassociate_address(self, context, public_ip, **kwargs):
|
def disassociate_address(self, context, public_ip, **kwargs):
|
||||||
address = self._get_address(public_ip)
|
address = self._get_address(public_ip)
|
||||||
rv = self.network.disassociate_address(public_ip)
|
self.network.disassociate_address(public_ip)
|
||||||
# TODO - Strip the IP from the instance
|
# TODO - Strip the IP from the instance
|
||||||
return defer.succeed({'disassociateResponse': ["Address disassociated."]})
|
return defer.succeed({'disassociateResponse': ["Address disassociated."]})
|
||||||
|
|
||||||
@@ -466,14 +453,10 @@ class CloudController(object):
|
|||||||
inst['project_id'] = context.project.id
|
inst['project_id'] = context.project.id
|
||||||
inst['mac_address'] = utils.generate_mac()
|
inst['mac_address'] = utils.generate_mac()
|
||||||
inst['ami_launch_index'] = num
|
inst['ami_launch_index'] = num
|
||||||
address, _netname = self.network.allocate_address(
|
address = network.allocate_ip(
|
||||||
user_id=inst['user_id'],
|
inst['user_id'], inst['project_id'], mac=inst['mac_address'])
|
||||||
project_id=inst['project_id'],
|
|
||||||
mac=inst['mac_address'])
|
|
||||||
network = self.network.get_users_network(str(context.user.id))
|
|
||||||
inst['network_str'] = json.dumps(network.to_dict())
|
|
||||||
inst['bridge_name'] = network.bridge_name
|
|
||||||
inst['private_dns_name'] = str(address)
|
inst['private_dns_name'] = str(address)
|
||||||
|
inst['bridge_name'] = network.BridgedNetwork.get_network_for_project(inst['user_id'], inst['project_id'])['bridge_name']
|
||||||
# TODO: allocate expresses on the router node
|
# TODO: allocate expresses on the router node
|
||||||
inst.save()
|
inst.save()
|
||||||
rpc.cast(FLAGS.compute_topic,
|
rpc.cast(FLAGS.compute_topic,
|
||||||
@@ -502,7 +485,7 @@ class CloudController(object):
|
|||||||
if instance.get('private_dns_name', None):
|
if instance.get('private_dns_name', None):
|
||||||
logging.debug("Deallocating address %s" % instance.get('private_dns_name', None))
|
logging.debug("Deallocating address %s" % instance.get('private_dns_name', None))
|
||||||
try:
|
try:
|
||||||
self.network.deallocate_address(instance.get('private_dns_name', None))
|
self.network.deallocate_ip(instance.get('private_dns_name', None))
|
||||||
except Exception, _err:
|
except Exception, _err:
|
||||||
pass
|
pass
|
||||||
if instance.get('node_name', 'unassigned') != 'unassigned': #It's also internal default
|
if instance.get('node_name', 'unassigned') != 'unassigned': #It's also internal default
|
||||||
|
|||||||
@@ -23,11 +23,17 @@ from nova import flags
|
|||||||
from nova import test
|
from nova import test
|
||||||
from nova.compute import network
|
from nova.compute import network
|
||||||
from nova.auth import users
|
from nova.auth import users
|
||||||
|
from nova import utils
|
||||||
|
|
||||||
|
|
||||||
class NetworkTestCase(test.TrialTestCase):
|
class NetworkTestCase(test.TrialTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(NetworkTestCase, self).setUp()
|
super(NetworkTestCase, self).setUp()
|
||||||
|
self.flags(fake_libvirt=True,
|
||||||
|
fake_storage=True,
|
||||||
|
fake_network=True,
|
||||||
|
network_size=32,
|
||||||
|
redis_db=8)
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
self.manager = users.UserManager.instance()
|
self.manager = users.UserManager.instance()
|
||||||
try:
|
try:
|
||||||
@@ -37,7 +43,7 @@ class NetworkTestCase(test.TrialTestCase):
|
|||||||
name = 'project%s' % i
|
name = 'project%s' % i
|
||||||
if not self.manager.get_project(name):
|
if not self.manager.get_project(name):
|
||||||
self.manager.create_project(name, 'netuser', name)
|
self.manager.create_project(name, 'netuser', name)
|
||||||
self.network = network.NetworkController(netsize=16)
|
self.network = network.PublicNetworkController()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(NetworkTestCase, self).tearDown()
|
super(NetworkTestCase, self).tearDown()
|
||||||
@@ -46,70 +52,69 @@ class NetworkTestCase(test.TrialTestCase):
|
|||||||
self.manager.delete_project(name)
|
self.manager.delete_project(name)
|
||||||
self.manager.delete_user('netuser')
|
self.manager.delete_user('netuser')
|
||||||
|
|
||||||
def test_network_serialization(self):
|
def test_public_network_allocation(self):
|
||||||
net1 = network.Network(vlan=100, network="192.168.100.0/24", conn=None)
|
pubnet = IPy.IP(flags.FLAGS.public_range)
|
||||||
address = net1.allocate_ip("netuser", "project0", "01:24:55:36:f2:a0")
|
address = self.network.allocate_ip("netuser", "project0", "public")
|
||||||
net_json = str(net1)
|
self.assertTrue(IPy.IP(address) in pubnet)
|
||||||
net2 = network.Network.from_json(net_json)
|
self.assertTrue(IPy.IP(address) in self.network.network)
|
||||||
self.assertEqual(net_json, str(net2))
|
|
||||||
self.assertTrue(IPy.IP(address) in net2.network)
|
|
||||||
|
|
||||||
def test_allocate_deallocate_address(self):
|
def test_allocate_deallocate_ip(self):
|
||||||
(address, net_name) = self.network.allocate_address("netuser",
|
address = network.allocate_ip(
|
||||||
"project0", "01:24:55:36:f2:a0")
|
"netuser", "project0", utils.generate_mac())
|
||||||
logging.debug("Was allocated %s" % (address))
|
logging.debug("Was allocated %s" % (address))
|
||||||
self.assertEqual(True, address in self._get_project_addresses("project0"))
|
self.assertEqual(True, address in self._get_project_addresses("project0"))
|
||||||
rv = self.network.deallocate_address(address)
|
rv = network.deallocate_ip(address)
|
||||||
self.assertEqual(False, address in self._get_project_addresses("project0"))
|
self.assertEqual(False, address in self._get_project_addresses("project0"))
|
||||||
|
|
||||||
def test_range_allocation(self):
|
def test_range_allocation(self):
|
||||||
(address, net_name) = self.network.allocate_address("netuser",
|
address = network.allocate_ip(
|
||||||
"project0", "01:24:55:36:f2:a0")
|
"netuser", "project0", utils.generate_mac())
|
||||||
(secondaddress, net_name) = self.network.allocate_address("netuser",
|
secondaddress = network.allocate_ip(
|
||||||
"project1", "01:24:55:36:f2:a0")
|
"netuser", "project1", utils.generate_mac())
|
||||||
self.assertEqual(True, address in self._get_project_addresses("project0"))
|
self.assertEqual(True,
|
||||||
|
address in self._get_project_addresses("project0"))
|
||||||
self.assertEqual(True,
|
self.assertEqual(True,
|
||||||
secondaddress in self._get_project_addresses("project1"))
|
secondaddress in self._get_project_addresses("project1"))
|
||||||
self.assertEqual(False, address in self._get_project_addresses("project1"))
|
self.assertEqual(False, address in self._get_project_addresses("project1"))
|
||||||
rv = self.network.deallocate_address(address)
|
rv = network.deallocate_ip(address)
|
||||||
self.assertEqual(False, address in self._get_project_addresses("project0"))
|
self.assertEqual(False, address in self._get_project_addresses("project0"))
|
||||||
rv = self.network.deallocate_address(secondaddress)
|
rv = network.deallocate_ip(secondaddress)
|
||||||
self.assertEqual(False,
|
self.assertEqual(False,
|
||||||
secondaddress in self._get_project_addresses("project1"))
|
secondaddress in self._get_project_addresses("project1"))
|
||||||
|
|
||||||
def test_subnet_edge(self):
|
def test_subnet_edge(self):
|
||||||
(secondaddress, net_name) = self.network.allocate_address("netuser", "project0")
|
secondaddress = network.allocate_ip("netuser", "project0",
|
||||||
|
utils.generate_mac())
|
||||||
for project in range(1,5):
|
for project in range(1,5):
|
||||||
project_id = "project%s" % (project)
|
project_id = "project%s" % (project)
|
||||||
(address, net_name) = self.network.allocate_address("netuser",
|
address = network.allocate_ip(
|
||||||
project_id, "01:24:55:36:f2:a0")
|
"netuser", project_id, utils.generate_mac())
|
||||||
(address2, net_name) = self.network.allocate_address("netuser",
|
address2 = network.allocate_ip(
|
||||||
project_id, "01:24:55:36:f2:a0")
|
"netuser", project_id, utils.generate_mac())
|
||||||
(address3, net_name) = self.network.allocate_address("netuser",
|
address3 = network.allocate_ip(
|
||||||
project_id, "01:24:55:36:f2:a0")
|
"netuser", project_id, utils.generate_mac())
|
||||||
self.assertEqual(False,
|
self.assertEqual(False,
|
||||||
address in self._get_project_addresses("project0"))
|
address in self._get_project_addresses("project0"))
|
||||||
self.assertEqual(False,
|
self.assertEqual(False,
|
||||||
address2 in self._get_project_addresses("project0"))
|
address2 in self._get_project_addresses("project0"))
|
||||||
self.assertEqual(False,
|
self.assertEqual(False,
|
||||||
address3 in self._get_project_addresses("project0"))
|
address3 in self._get_project_addresses("project0"))
|
||||||
rv = self.network.deallocate_address(address)
|
rv = network.deallocate_ip(address)
|
||||||
rv = self.network.deallocate_address(address2)
|
rv = network.deallocate_ip(address2)
|
||||||
rv = self.network.deallocate_address(address3)
|
rv = network.deallocate_ip(address3)
|
||||||
rv = self.network.deallocate_address(secondaddress)
|
rv = network.deallocate_ip(secondaddress)
|
||||||
|
|
||||||
def test_too_many_projects(self):
|
def test_too_many_projects(self):
|
||||||
for i in range(0, 30):
|
for i in range(0, 30):
|
||||||
name = 'toomany-project%s' % i
|
name = 'toomany-project%s' % i
|
||||||
self.manager.create_project(name, 'netuser', name)
|
self.manager.create_project(name, 'netuser', name)
|
||||||
(address, net_name) = self.network.allocate_address("netuser",
|
address = network.allocate_ip(
|
||||||
name, "01:24:55:36:f2:a0")
|
"netuser", name, utils.generate_mac())
|
||||||
|
rv = network.deallocate_ip(address)
|
||||||
self.manager.delete_project(name)
|
self.manager.delete_project(name)
|
||||||
|
|
||||||
def _get_project_addresses(self, project_id):
|
def _get_project_addresses(self, project_id):
|
||||||
rv = self.network.describe_addresses()
|
|
||||||
project_addresses = []
|
project_addresses = []
|
||||||
for item in rv:
|
for addr in network.get_project_network(project_id).list_addresses():
|
||||||
if item['project_id'] == project_id:
|
project_addresses.append(addr)
|
||||||
project_addresses.append(item['address'])
|
|
||||||
return project_addresses
|
return project_addresses
|
||||||
|
|||||||
Reference in New Issue
Block a user