summaryrefslogtreecommitdiffstats
path: root/nova/compute
diff options
context:
space:
mode:
authorJesse Andrews <anotherjesse@gmail.com>2010-05-27 23:05:26 -0700
committerJesse Andrews <anotherjesse@gmail.com>2010-05-27 23:05:26 -0700
commitbf6e6e718cdc7488e2da87b21e258ccc065fe499 (patch)
tree51cf4f72047eb6b16079c7fe21e9822895541801 /nova/compute
initial commit
Diffstat (limited to 'nova/compute')
-rw-r--r--nova/compute/__init__.py28
-rw-r--r--nova/compute/disk.py122
-rw-r--r--nova/compute/exception.py35
-rw-r--r--nova/compute/fakevirtinstance.xml43
-rw-r--r--nova/compute/libvirt.xml.template46
-rw-r--r--nova/compute/linux_net.py146
-rw-r--r--nova/compute/model.py203
-rw-r--r--nova/compute/network.py520
-rw-r--r--nova/compute/node.py549
9 files changed, 1692 insertions, 0 deletions
diff --git a/nova/compute/__init__.py b/nova/compute/__init__.py
new file mode 100644
index 000000000..e8a6921e7
--- /dev/null
+++ b/nova/compute/__init__.py
@@ -0,0 +1,28 @@
+# 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.
+
+"""
+:mod:`nova.compute` -- Compute Nodes using LibVirt
+=====================================================
+
+.. automodule:: nova.compute
+ :platform: Unix
+ :synopsis: Thin wrapper around libvirt for VM mgmt.
+.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
+.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com>
+.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
+.. moduleauthor:: Joshua McKenty <joshua@cognition.ca>
+.. moduleauthor:: Manish Singh <yosh@gimp.org>
+.. moduleauthor:: Andy Smith <andy@anarkystic.com>
+""" \ No newline at end of file
diff --git a/nova/compute/disk.py b/nova/compute/disk.py
new file mode 100644
index 000000000..d3eeb951f
--- /dev/null
+++ b/nova/compute/disk.py
@@ -0,0 +1,122 @@
+# 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.
+
+"""
+Utility methods to resize, repartition, and modify disk images.
+Includes injection of SSH PGP keys into authorized_keys file.
+"""
+
+import logging
+import os
+import tempfile
+
+from nova.exception import Error
+from nova.utils import execute
+
+def partition(infile, outfile, local_bytes=0, local_type='ext2'):
+ """Takes a single partition represented by infile and writes a bootable drive image into outfile.
+ The first 63 sectors (0-62) of the resulting image is a master boot record.
+ Infile becomes the first primary partition.
+ If local bytes is specified, a second primary partition is created and formatted as ext2.
+ In the diagram below, dashes represent drive sectors.
+ 0 a b c d e
+ +-----+------. . .-------+------. . .------+
+ | mbr | primary partiton | local partition |
+ +-----+------. . .-------+------. . .------+
+ """
+ sector_size = 512
+ file_size = os.path.getsize(infile)
+ if file_size % sector_size != 0:
+ logging.warn("Input partition size not evenly divisible by sector size: %d / %d" (file_size, sector_size))
+ primary_sectors = file_size / sector_size
+ if local_bytes % sector_size != 0:
+ logging.warn("Bytes for local storage not evenly divisible by sector size: %d / %d" (local_bytes, sector_size))
+ local_sectors = local_bytes / sector_size
+
+ mbr_last = 62 # a
+ primary_first = mbr_last + 1 # b
+ primary_last = primary_first + primary_sectors # c
+ local_first = primary_last + 1 # d
+ local_last = local_first + local_sectors # e
+ last_sector = local_last # e
+
+ # create an empty file
+ execute('dd if=/dev/zero of=%s count=1 seek=%d bs=%d' % (outfile, last_sector, sector_size))
+
+ # make mbr partition
+ execute('parted --script %s mklabel msdos' % outfile)
+
+ # make primary partition
+ execute('parted --script %s mkpart primary %ds %ds' % (outfile, primary_first, primary_last))
+
+ # make local partition
+ if local_bytes > 0:
+ execute('parted --script %s mkpartfs primary %s %ds %ds' % (outfile, local_type, local_first, local_last))
+
+ # copy file into partition
+ execute('dd if=%s of=%s bs=%d seek=%d conv=notrunc,fsync' % (infile, outfile, sector_size, primary_first))
+
+
+def inject_key(key, image, partition=None):
+ """Injects a ssh key into a disk image.
+ It adds the specified key to /root/.ssh/authorized_keys
+ it will mount the image as a fully partitioned disk and attempt to inject into the specified partition number.
+ If partition is not specified it mounts the image as a single partition.
+ """
+ out, err = execute('sudo losetup -f --show %s' % image)
+ if err:
+ raise Error('Could not attach image to loopback: %s' % err)
+ device = out.strip()
+ try:
+ if not partition is None:
+ # create partition
+ out, err = execute('sudo kpartx -a %s' % device)
+ if err:
+ raise Error('Failed to load partition: %s' % err)
+ mapped_device = '/dev/mapper/%sp%s' % ( device.split('/')[-1] , partition )
+ else:
+ mapped_device = device
+ out, err = execute('sudo tune2fs -c 0 -i 0 %s' % mapped_device)
+
+ tmpdir = tempfile.mkdtemp()
+ try:
+ # mount loopback to dir
+ out, err = execute('sudo mount %s %s' % (mapped_device, tmpdir))
+ if err:
+ raise Error('Failed to mount filesystem: %s' % err)
+
+ try:
+ # inject key file
+ _inject_into_fs(key, tmpdir)
+ finally:
+ # unmount device
+ execute('sudo umount %s' % mapped_device)
+ finally:
+ # remove temporary directory
+ os.rmdir(tmpdir)
+ if not partition is None:
+ # remove partitions
+ execute('sudo kpartx -d %s' % device)
+ finally:
+ # remove loopback
+ execute('sudo losetup -d %s' % device)
+
+def _inject_into_fs(key, fs):
+ sshdir = os.path.join(os.path.join(fs, 'root'), '.ssh')
+ execute('sudo mkdir %s' % sshdir) #error on existing dir doesn't matter
+ execute('sudo chown root %s' % sshdir)
+ execute('sudo chmod 700 %s' % sshdir)
+ keyfile = os.path.join(sshdir, 'authorized_keys')
+ execute('sudo bash -c "cat >> %s"' % keyfile, '\n' + key + '\n')
+
diff --git a/nova/compute/exception.py b/nova/compute/exception.py
new file mode 100644
index 000000000..6fe8e381f
--- /dev/null
+++ b/nova/compute/exception.py
@@ -0,0 +1,35 @@
+# 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.
+
+"""
+Exceptions for Compute Node errors, mostly network addressing.
+"""
+
+from nova.exception import Error
+
+class NoMoreAddresses(Error):
+ pass
+
+class AddressNotAllocated(Error):
+ pass
+
+class AddressAlreadyAssociated(Error):
+ pass
+
+class AddressNotAssociated(Error):
+ pass
+
+class NotValidNetworkSize(Error):
+ pass
+
diff --git a/nova/compute/fakevirtinstance.xml b/nova/compute/fakevirtinstance.xml
new file mode 100644
index 000000000..6036516bb
--- /dev/null
+++ b/nova/compute/fakevirtinstance.xml
@@ -0,0 +1,43 @@
+<!--
+# 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.
+ -->
+<domain type='kvm' id='100'>
+ <name>i-A9B8C7D6</name>
+ <uuid>12a345bc-67c8-901d-2e34-56f7g89012h3</uuid>
+ <memory>524288</memory>
+ <currentMemory>524288</currentMemory>
+ <vcpu>1</vcpu>
+ <os/>
+ <features>
+ <acpi/>
+ </features>
+ <clock offset='utc'/>
+ <on_poweroff>destroy</on_poweroff>
+ <on_reboot>restart</on_reboot>
+ <on_crash>destroy</on_crash>
+ <devices>
+ <emulator>/usr/bin/kvm</emulator>
+ <disk type='file' device='disk'>
+ <source file='/var/lib/fakevirt/instances/i-A9B8C7D6/disk'/>
+ <target dev='sda' bus='scsi'/>
+ </disk>
+ <interface type='bridge'>
+ <mac address='a0:1b:c2:3d:4e:f5'/>
+ <source bridge='fakebr2000'/>
+ <target dev='vnet1'/>
+ <model type='e1000'/>
+ </interface>
+ </devices>
+</domain> \ No newline at end of file
diff --git a/nova/compute/libvirt.xml.template b/nova/compute/libvirt.xml.template
new file mode 100644
index 000000000..4cf6e8b10
--- /dev/null
+++ b/nova/compute/libvirt.xml.template
@@ -0,0 +1,46 @@
+<!--
+# 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.
+ -->
+<domain type='kvm'>
+ <name>%(name)s</name>
+ <os>
+ <type>hvm</type>
+ <kernel>%(basepath)s/kernel</kernel>
+ <initrd>%(basepath)s/ramdisk</initrd>
+ <cmdline>root=/dev/vda1 console=ttyS0</cmdline>
+ </os>
+ <features>
+ <acpi/>
+ </features>
+ <memory>%(memory_kb)s</memory>
+ <vcpu>%(vcpus)s</vcpu>
+ <devices>
+ <emulator>/usr/bin/kvm</emulator>
+ <disk type='file'>
+ <source file='%(basepath)s/disk'/>
+ <target dev='vda' bus='virtio'/>
+ </disk>
+ <interface type='bridge'>
+ <source bridge='%(bridge_name)s'/>
+ <mac address='%(mac_address)s'/>
+ <!-- <model type='virtio'/> CANT RUN virtio network right now -->
+ </interface>
+ <serial type="file">
+ <source path='%(basepath)s/console.log'/>
+ <target port='1'/>
+ </serial>
+ </devices>
+ <nova>%(nova)s</nova>
+</domain>
diff --git a/nova/compute/linux_net.py b/nova/compute/linux_net.py
new file mode 100644
index 000000000..0983241f9
--- /dev/null
+++ b/nova/compute/linux_net.py
@@ -0,0 +1,146 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+import signal
+import os
+import nova.utils
+import subprocess
+
+# todo(ja): does the definition of network_path belong here?
+
+from nova import flags
+FLAGS=flags.FLAGS
+
+def execute(cmd):
+ if FLAGS.fake_network:
+ print "FAKE NET: %s" % cmd
+ return "fake", 0
+ else:
+ nova.utils.execute(cmd)
+
+def runthis(desc, cmd):
+ if FLAGS.fake_network:
+ execute(cmd)
+ else:
+ nova.utils.runthis(desc,cmd)
+
+def Popen(cmd):
+ if FLAGS.fake_network:
+ execute(' '.join(cmd))
+ else:
+ subprocess.Popen(cmd)
+
+
+def device_exists(device):
+ (out, err) = execute("ifconfig %s" % device)
+ return not err
+
+def confirm_rule(cmd):
+ execute("sudo iptables --delete %s" % (cmd))
+ execute("sudo iptables -I %s" % (cmd))
+
+def remove_rule(cmd):
+ execute("sudo iptables --delete %s" % (cmd))
+
+def bind_public_ip(ip, interface):
+ runthis("Binding IP to interface: %s", "sudo ip addr add %s dev %s" % (ip, interface))
+
+def vlan_create(net):
+ """ create a vlan on on a bridge device unless vlan already exists """
+ if not device_exists("vlan%s" % net.vlan):
+ execute("sudo vconfig set_name_type VLAN_PLUS_VID_NO_PAD")
+ execute("sudo vconfig add %s %s" % (net.bridge_dev, net.vlan))
+ execute("sudo ifconfig vlan%s up" % (net.vlan))
+
+def bridge_create(net):
+ """ create a bridge on a vlan unless it already exists """
+ if not device_exists(net.bridge_name):
+ execute("sudo brctl addbr %s" % (net.bridge_name))
+ # execute("sudo brctl setfd %s 0" % (net.bridge_name))
+ # execute("sudo brctl setageing %s 10" % (net.bridge_name))
+ execute("sudo brctl stp %s off" % (net.bridge_name))
+ execute("sudo brctl addif %s vlan%s" % (net.bridge_name, net.vlan))
+ if net.bridge_gets_ip:
+ execute("sudo ifconfig %s %s broadcast %s netmask %s up" % \
+ (net.bridge_name, net.gateway, net.broadcast, net.netmask))
+ confirm_rule("FORWARD --in-interface %s -j ACCEPT" % (net.bridge_name))
+ else:
+ execute("sudo ifconfig %s up" % net.bridge_name)
+
+def dnsmasq_cmd(net):
+ cmd = ['sudo dnsmasq',
+ ' --strict-order',
+ ' --bind-interfaces',
+ ' --conf-file=',
+ ' --pid-file=%s' % dhcp_file(net.vlan, 'pid'),
+ ' --listen-address=%s' % net.dhcp_listen_address,
+ ' --except-interface=lo',
+ ' --dhcp-range=%s,%s,120s' % (net.dhcp_range_start, net.dhcp_range_end),
+ ' --dhcp-lease-max=61',
+ ' --dhcp-hostsfile=%s' % dhcp_file(net.vlan, 'conf'),
+ ' --dhcp-leasefile=%s' % dhcp_file(net.vlan, 'leases')]
+ return ''.join(cmd)
+
+def hostDHCP(network, host):
+ idx = host['address'].split(".")[-1] # Logically, the idx of instances they've launched in this net
+ return "%s,%s-%s-%s.novalocal,%s" % \
+ (host['mac'], host['user_id'], network.vlan, idx, host['address'])
+
+# todo(ja): if the system has restarted or pid numbers have wrapped
+# then you cannot be certain that the pid refers to the
+# dnsmasq. As well, sending a HUP only reloads the hostfile,
+# so any configuration options (like dchp-range, vlan, ...)
+# aren't reloaded
+def start_dnsmasq(network):
+ """ (re)starts a dnsmasq server for a given network
+
+ if a dnsmasq instance is already running then send a HUP
+ signal causing it to reload, otherwise spawn a new instance
+ """
+ with open(dhcp_file(network.vlan, 'conf'), 'w') as f:
+ for host_name in network.hosts:
+ f.write("%s\n" % hostDHCP(network, network.hosts[host_name]))
+
+ pid = dnsmasq_pid_for(network)
+
+ # if dnsmasq is already running, then tell it to reload
+ if pid:
+ # todo(ja): use "/proc/%d/cmdline" % (pid) to determine if pid refers
+ # correct dnsmasq process
+ try:
+ os.kill(pid, signal.SIGHUP)
+ return
+ except Exception, e:
+ logging.debug("Killing dnsmasq threw %s", e)
+
+ # otherwise delete the existing leases file and start dnsmasq
+ lease_file = dhcp_file(network.vlan, 'leases')
+ if os.path.exists(lease_file):
+ os.unlink(lease_file)
+
+ Popen(dnsmasq_cmd(network).split(" "))
+
+def stop_dnsmasq(network):
+ """ stops the dnsmasq instance for a given network """
+ pid = dnsmasq_pid_for(network)
+
+ if pid:
+ os.kill(pid, signal.SIGTERM)
+
+def dhcp_file(vlan, kind):
+ """ return path to a pid, leases or conf file for a vlan """
+
+ return os.path.abspath("%s/nova-%s.%s" % (FLAGS.networks_path, vlan, kind))
+
+def dnsmasq_pid_for(network):
+ """ the pid for prior dnsmasq instance for a vlan,
+ returns None if no pid file exists
+
+ if machine has rebooted pid might be incorrect (caller should check)
+ """
+
+ pid_file = dhcp_file(network.vlan, 'pid')
+
+ if os.path.exists(pid_file):
+ with open(pid_file, 'r') as f:
+ return int(f.read())
+
diff --git a/nova/compute/model.py b/nova/compute/model.py
new file mode 100644
index 000000000..78ed3a101
--- /dev/null
+++ b/nova/compute/model.py
@@ -0,0 +1,203 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab
+# 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.
+
+"""
+Datastore Model objects for Compute Instances, with
+InstanceDirectory manager.
+
+# Create a new instance?
+>>> InstDir = InstanceDirectory()
+>>> inst = InstDir.new()
+>>> inst.destroy()
+True
+>>> inst = InstDir['i-123']
+>>> inst['ip'] = "192.168.0.3"
+>>> inst['owner_id'] = "projectA"
+>>> inst.save()
+True
+
+>>> InstDir['i-123']
+<Instance:i-123>
+>>> InstDir.all.next()
+<Instance:i-123>
+
+>>> inst.destroy()
+True
+"""
+
+from nova import vendor
+
+from nova import datastore
+from nova import flags
+from nova import utils
+
+
+FLAGS = flags.FLAGS
+
+
+# TODO(ja): singleton instance of the directory
+class InstanceDirectory(object):
+ """an api for interacting with the global state of instances """
+ def __init__(self):
+ self.keeper = datastore.Keeper(FLAGS.instances_prefix)
+
+ def get(self, instance_id):
+ """ returns an instance object for a given id """
+ return Instance(instance_id)
+
+ def __getitem__(self, item):
+ return self.get(item)
+
+ def by_project(self, project):
+ """ returns a list of instance objects for a project """
+ for instance_id in self.keeper['project:%s:instances' % project]:
+ yield Instance(instance_id)
+
+ def by_node(self, node_id):
+ """ returns a list of instances for a node """
+ for instance in self.all:
+ if instance['node_name'] == node_id:
+ yield instance
+
+ def by_ip(self, ip_address):
+ """ returns an instance object that is using the IP """
+ for instance in self.all:
+ if instance['private_dns_name'] == ip_address:
+ return instance
+ return None
+
+ def by_volume(self, volume_id):
+ """ returns the instance a volume is attached to """
+ pass
+
+ def exists(self, instance_id):
+ if instance_id in self.keeper['instances']:
+ return True
+ return False
+
+ @property
+ def all(self):
+ """ returns a list of all instances """
+ instances = self.keeper['instances']
+ if instances != None:
+ for instance_id in self.keeper['instances']:
+ yield Instance(instance_id)
+
+ def new(self):
+ """ returns an empty Instance object, with ID """
+ instance_id = utils.generate_uid('i')
+ return self.get(instance_id)
+
+
+
+class Instance(object):
+ """ Wrapper around stored properties of an instance """
+
+ def __init__(self, instance_id):
+ """ loads an instance from the datastore if exists """
+ self.keeper = datastore.Keeper(FLAGS.instances_prefix)
+ self.instance_id = instance_id
+ self.initial_state = {}
+ self.state = self.keeper[self.__redis_key]
+ if self.state:
+ self.initial_state = self.state
+ else:
+ self.state = {'state' : 'pending',
+ 'instance_id' : instance_id,
+ 'node_name' : 'unassigned',
+ 'owner_id' : 'unassigned' }
+
+ @property
+ def __redis_key(self):
+ """ Magic string for instance keys """
+ return 'instance:%s' % self.instance_id
+
+ def __repr__(self):
+ return "<Instance:%s>" % self.instance_id
+
+ def get(self, item, default):
+ return self.state.get(item, default)
+
+ def __getitem__(self, item):
+ return self.state[item]
+
+ def __setitem__(self, item, val):
+ self.state[item] = val
+ return self.state[item]
+
+ def __delitem__(self, item):
+ """ We don't support this """
+ raise Exception("Silly monkey, Instances NEED all their properties.")
+
+ def save(self):
+ """ update the directory with the state from this instance
+ make sure you've set the owner_id before you call save
+ for the first time.
+ """
+ # TODO(ja): implement hmset in redis-py and use it
+ # instead of multiple calls to hset
+ state = self.keeper[self.__redis_key]
+ if not state:
+ state = {}
+ for key, val in self.state.iteritems():
+ # if (not self.initial_state.has_key(key)
+ # or self.initial_state[key] != val):
+ state[key] = val
+ self.keeper[self.__redis_key] = state
+ if self.initial_state == {}:
+ self.keeper.set_add('project:%s:instances' % self.state['owner_id'],
+ self.instance_id)
+ self.keeper.set_add('instances', self.instance_id)
+ self.initial_state = self.state
+ return True
+
+ def destroy(self):
+ """ deletes all related records from datastore.
+ does NOT do anything to running libvirt state.
+ """
+ self.keeper.set_remove('project:%s:instances' % self.state['owner_id'],
+ self.instance_id)
+ del self.keeper[self.__redis_key]
+ self.keeper.set_remove('instances', self.instance_id)
+ return True
+
+ @property
+ def volumes(self):
+ """ returns a list of attached volumes """
+ pass
+
+ @property
+ def reservation(self):
+ """ Returns a reservation object """
+ pass
+
+# class Reservation(object):
+# """ ORM wrapper for a batch of launched instances """
+# def __init__(self):
+# pass
+#
+# def userdata(self):
+# """ """
+# pass
+#
+#
+# class NodeDirectory(object):
+# def __init__(self):
+# pass
+#
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/nova/compute/network.py b/nova/compute/network.py
new file mode 100644
index 000000000..612295f27
--- /dev/null
+++ b/nova/compute/network.py
@@ -0,0 +1,520 @@
+# 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.
+
+"""
+Classes for network control, including VLANs, DHCP, and IP allocation.
+"""
+
+import json
+import logging
+import os
+
+# TODO(termie): clean up these imports
+from nova import vendor
+import IPy
+
+from nova import datastore
+import nova.exception
+from nova.compute import exception
+from nova import flags
+from nova import utils
+from nova.auth import users
+
+import linux_net
+
+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'),
+ 'Location to keep network config files')
+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('bridge_dev', 'eth1',
+ 'network device for bridges')
+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('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('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)
+
+# 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):
+ def __init__(self, *args, **kwargs):
+ self.bridge_gets_ip = False
+ 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
+ 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 range(self):
+ for idx in range(3, len(self.network)-2):
+ yield self.network[idx]
+
+ def allocate_ip(self, user_id, mac):
+ for ip in self.range():
+ address = str(ip)
+ if not address in self.hosts.keys():
+ logging.debug("Allocating IP %s to %s" % (address, user_id))
+ self.hosts[address] = {
+ "address" : address, "user_id" : user_id, 'mac' : mac
+ }
+ self.express(address=address)
+ return address
+ raise exception.NoMoreAddresses()
+
+ def deallocate_ip(self, ip_str):
+ if not ip_str in self.hosts.keys():
+ raise exception.AddressNotAllocated()
+ del self.hosts[ip_str]
+ # TODO(joshua) SCRUB from the leases file somehow
+ self.deexpress(address=ip_str)
+
+ def list_addresses(self):
+ for address in self.hosts.values():
+ yield address
+
+ def express(self, address=None):
+ pass
+
+ def deexpress(self, address=None):
+ pass
+
+
+class Vlan(Network):
+ """
+ VLAN configuration, that when expressed creates the vlan
+
+ 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:
+ bridge_name - string (example value: br42)
+ vlan - integer (example value: 42)
+ bridge_gets_ip - boolean used during bridge creation
+
+ if bridge_gets_ip then network address for bridge uses the properties:
+ gateway
+ broadcast
+ netmask
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(VirtNetwork, self).__init__(*args, **kwargs)
+
+ def virtXML(self):
+ """ 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):
+ """ creates a bridge device on top of the Vlan """
+ super(VirtNetwork, self).express(address=address)
+ try:
+ logging.debug("Starting Bridge inteface for %s network" % (self.vlan))
+ linux_net.bridge_create(self)
+ except:
+ pass
+
+class DHCPNetwork(VirtNetwork):
+ """
+ properties:
+ dhcp_listen_address: the ip of the gateway / dhcp host
+ dhcp_range_start: the first ip to give out
+ dhcp_range_end: the last ip to give out
+ """
+ def __init__(self, *args, **kwargs):
+ super(DHCPNetwork, self).__init__(*args, **kwargs)
+ logging.debug("Initing DHCPNetwork object...")
+ self.bridge_gets_ip = True
+ self.dhcp_listen_address = self.network[1]
+ self.dhcp_range_start = self.network[3]
+ self.dhcp_range_end = self.network[-2]
+
+ def express(self, address=None):
+ super(DHCPNetwork, self).express(address=address)
+ if len(self.hosts.values()) > 0:
+ logging.debug("Starting dnsmasq server for network with vlan %s" % self.vlan)
+ linux_net.start_dnsmasq(self)
+ else:
+ logging.debug("Not launching dnsmasq cause I don't think we have any hosts.")
+
+ def deexpress(self, address=None):
+ # if this is the last address, stop dns
+ super(DHCPNetwork, self).deexpress(address=address)
+ if len(self.hosts.values()) == 0:
+ linux_net.stop_dnsmasq(self)
+ else:
+ linux_net.start_dnsmasq(self)
+
+
+class PrivateNetwork(DHCPNetwork):
+ def __init__(self, **kwargs):
+ super(PrivateNetwork, self).__init__(**kwargs)
+ # self.express()
+
+ def to_dict(self):
+ return {'vlan': self.vlan,
+ 'network': self.network_str,
+ 'hosts': self.hosts}
+
+ def express(self, *args, **kwargs):
+ super(PrivateNetwork, self).express(*args, **kwargs)
+
+
+
+class PublicNetwork(Network):
+ def __init__(self, network="192.168.216.0/24", **kwargs):
+ super(PublicNetwork, self).__init__(network=network, **kwargs)
+ self.express()
+
+ def allocate_ip(self, user_id, mac):
+ for ip in self.range():
+ address = str(ip)
+ if not address in self.hosts.keys():
+ logging.debug("Allocating IP %s to %s" % (address, user_id))
+ self.hosts[address] = {
+ "address" : address, "user_id" : user_id, 'mac' : mac
+ }
+ self.express(address=address)
+ return address
+ raise exception.NoMoreAddresses()
+
+ def deallocate_ip(self, ip_str):
+ if not ip_str in self.hosts:
+ raise exception.AddressNotAllocated()
+ del self.hosts[ip_str]
+ # TODO(joshua) SCRUB from the leases file somehow
+ self.deexpress(address=ip_str)
+
+ def associate_address(self, public_ip, private_ip, instance_id):
+ if not public_ip in self.hosts:
+ raise exception.AddressNotAllocated()
+ for addr in self.hosts.values():
+ if addr.has_key('private_ip') and addr['private_ip'] == private_ip:
+ raise exception.AddressAlreadyAssociated()
+ if self.hosts[public_ip].has_key('private_ip'):
+ raise exception.AddressAlreadyAssociated()
+ self.hosts[public_ip]['private_ip'] = private_ip
+ self.hosts[public_ip]['instance_id'] = instance_id
+ self.express(address=public_ip)
+
+ def disassociate_address(self, public_ip):
+ if not public_ip in self.hosts:
+ raise exception.AddressNotAllocated()
+ if not self.hosts[public_ip].has_key('private_ip'):
+ raise exception.AddressNotAssociated()
+ self.deexpress(public_ip)
+ del self.hosts[public_ip]['private_ip']
+ del self.hosts[public_ip]['instance_id']
+ # TODO Express the removal
+
+ 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):
+ logging.debug("Todo - need to create IPTables natting entries for this net.")
+ addresses = self.hosts.values()
+ if address:
+ addresses = [self.hosts[address]]
+ for addr in addresses:
+ if not addr.has_key('private_ip'):
+ continue
+ public_ip = addr['address']
+ private_ip = addr['private_ip']
+ 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("POSTROUTING -t nat -s %s -j SNAT --to %s" % (private_ip, public_ip))
+ # TODO: Get these from the secgroup datastore entries
+ linux_net.confirm_rule("FORWARD -d %s -p icmp -j ACCEPT" % (private_ip))
+ for (protocol, port) in [("tcp",80), ("tcp",22), ("udp",1194), ("tcp",443)]:
+ linux_net.confirm_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT" % (private_ip, protocol, port))
+
+
+class NetworkPool(object):
+ # TODO - Allocations need to be system global
+
+ def __init__(self):
+ self.network = IPy.IP(FLAGS.private_range)
+ 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):
+ start = (vlan-self.startvlan) * self.netsize
+ net_str = "%s-%s" % (self.network[start], self.network[start + self.netsize - 1])
+ logging.debug("Allocating %s" % net_str)
+ return net_str
+
+
+class VlanPool(object):
+ def __init__(self, **kwargs):
+ self.start = FLAGS.vlan_start
+ self.end = FLAGS.vlan_end
+ self.vlans = kwargs.get('vlans', {})
+ self.vlanpool = {}
+ self.manager = users.UserManager.instance()
+ for user_id, vlan in self.vlans.iteritems():
+ self.vlanpool[vlan] = user_id
+
+ def to_dict(self):
+ return {'vlans': self.vlans}
+
+ def __str__(self):
+ return json.dumps(self.to_dict())
+
+ def __unicode__(self):
+ 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, user_id, vlan):
+ logging.debug("Assigning vlan %s to user %s" % (vlan, user_id))
+ self.vlans[user_id] = vlan
+ self.vlanpool[vlan] = user_id
+ return self.vlans[user_id]
+
+ def next(self, user_id):
+ for old_user_id, vlan in self.vlans.iteritems():
+ if not self.manager.get_user(old_user_id):
+ _get_keeper()["%s-default" % old_user_id] = {}
+ del _get_keeper()["%s-default" % old_user_id]
+ del self.vlans[old_user_id]
+ return self.assign_vlan(user_id, vlan)
+ vlans = self.vlanpool.keys()
+ vlans.append(self.start)
+ nextvlan = max(vlans) + 1
+ if nextvlan == self.end:
+ raise exception.AddressNotAllocated("Out of VLANs")
+ return self.assign_vlan(user_id, nextvlan)
+
+
+class NetworkController(object):
+ """ The network controller is in charge of network connections """
+
+ def __init__(self, **kwargs):
+ logging.debug("Starting up the network controller.")
+ 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_users_network(self, user_id):
+ """ get a user's private network, allocating one if needed """
+
+ user = self.manager.get_user(user_id)
+ if not user:
+ raise Exception("User %s doesn't exist, uhoh." % user_id)
+ usernet = self.get_network_from_name("%s-default" % user_id)
+ if not usernet:
+ pool = self.vlan_pool
+ vlan = pool.next(user_id)
+ private_pool = NetworkPool()
+ network_str = private_pool.get_from_vlan(vlan)
+ logging.debug("Constructing network %s and %s for %s" % (network_str, vlan, user_id))
+ usernet = PrivateNetwork(
+ network=network_str,
+ vlan=vlan)
+ _get_keeper()["%s-default" % user_id] = usernet.to_dict()
+ _get_keeper()['vlans'] = pool.to_dict()
+ return usernet
+
+ def allocate_address(self, user_id, mac=None, type=PrivateNetwork):
+ ip = None
+ net_name = None
+ if type == PrivateNetwork:
+ net = self.get_users_network(user_id)
+ ip = net.allocate_ip(user_id, mac)
+ net_name = net.name
+ _get_keeper()["%s-default" % user_id] = net.to_dict()
+ else:
+ net = self.public_net
+ ip = net.allocate_ip(user_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 user in self.manager.get_users():
+ if address in self.get_users_network(user.id).network:
+ net = self.get_users_network(user.id)
+ rv = net.deallocate_ip(str(address))
+ _get_keeper()["%s-default" % user.id] = net.to_dict()
+ return rv
+ raise exception.AddressNotAllocated()
+
+ def describe_addresses(self, type=PrivateNetwork):
+ if type == PrivateNetwork:
+ addresses = []
+ for user in self.manager.get_users():
+ addresses.extend(self.get_users_network(user.id).list_addresses())
+ return addresses
+ return self.public_net.list_addresses()
+
+ def associate_address(self, address, private_ip, instance_id):
+ net = self.public_net
+ rv = net.associate_address(address, private_ip, instance_id)
+ _get_keeper()['public'] = net.to_dict()
+ return rv
+
+ def disassociate_address(self, address):
+ net = self.public_net
+ rv = net.disassociate_address(address)
+ _get_keeper()['public'] = net.to_dict()
+ return rv
+
+ def express(self,address=None):
+ for user in self.manager.get_users():
+ self.get_users_network(user.id).express()
+
+ def report_state(self):
+ pass
+
diff --git a/nova/compute/node.py b/nova/compute/node.py
new file mode 100644
index 000000000..a4de0f98a
--- /dev/null
+++ b/nova/compute/node.py
@@ -0,0 +1,549 @@
+# 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.
+
+"""
+Compute Node:
+
+ Runs on each compute node, managing the
+ hypervisor using libvirt.
+
+"""
+
+import base64
+import json
+import logging
+import os
+import random
+import shutil
+import sys
+
+from nova import vendor
+from twisted.internet import defer
+from twisted.internet import task
+from twisted.application import service
+
+try:
+ import libvirt
+except Exception, err:
+ logging.warning('no libvirt found')
+
+from nova import exception
+from nova import fakevirt
+from nova import flags
+from nova import process
+from nova import utils
+from nova.compute import disk
+from nova.compute import model
+from nova.compute import network
+from nova.objectstore import image # for image_path flag
+
+FLAGS = flags.FLAGS
+flags.DEFINE_string('libvirt_xml_template',
+ utils.abspath('compute/libvirt.xml.template'),
+ 'Network XML Template')
+flags.DEFINE_bool('use_s3', True,
+ 'whether to get images from s3 or use local copy')
+flags.DEFINE_string('instances_path', utils.abspath('../instances'),
+ 'where instances are stored on disk')
+flags.DEFINE_string('instances_prefix', 'compute-',
+ 'prefix for keepers for instances')
+
+INSTANCE_TYPES = {}
+INSTANCE_TYPES['m1.tiny'] = {'memory_mb': 512, 'vcpus': 1, 'local_gb': 0}
+INSTANCE_TYPES['m1.small'] = {'memory_mb': 1024, 'vcpus': 1, 'local_gb': 10}
+INSTANCE_TYPES['m1.medium'] = {'memory_mb': 2048, 'vcpus': 2, 'local_gb': 10}
+INSTANCE_TYPES['m1.large'] = {'memory_mb': 4096, 'vcpus': 4, 'local_gb': 10}
+INSTANCE_TYPES['m1.xlarge'] = {'memory_mb': 8192, 'vcpus': 4, 'local_gb': 10}
+INSTANCE_TYPES['c1.medium'] = {'memory_mb': 2048, 'vcpus': 4, 'local_gb': 10}
+
+# The number of processes to start in our process pool
+# TODO(termie): this should probably be a flag and the pool should probably
+# be a singleton
+PROCESS_POOL_SIZE = 4
+
+
+class Node(object, service.Service):
+ """
+ Manages the running instances.
+ """
+ def __init__(self):
+ """ load configuration options for this node and connect to libvirt """
+ super(Node, self).__init__()
+ self._instances = {}
+ self._conn = self._get_connection()
+ self._pool = process.Pool(PROCESS_POOL_SIZE)
+ self.instdir = model.InstanceDirectory()
+ # TODO(joshua): This needs to ensure system state, specifically: modprobe aoe
+
+ def _get_connection(self):
+ """ returns a libvirt connection object """
+ # TODO(termie): maybe lazy load after initial check for permissions
+ # TODO(termie): check whether we can be disconnected
+ if FLAGS.fake_libvirt:
+ conn = fakevirt.FakeVirtConnection.instance()
+ else:
+ auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT],
+ 'root',
+ None]
+ conn = libvirt.openAuth('qemu:///system', auth, 0)
+ if conn == None:
+ logging.error('Failed to open connection to the hypervisor')
+ sys.exit(1)
+ return conn
+
+ def noop(self):
+ """ simple test of an AMQP message call """
+ return defer.succeed('PONG')
+
+ def get_instance(self, instance_id):
+ # inst = self.instdir.get(instance_id)
+ # return inst
+ if self.instdir.exists(instance_id):
+ return Instance.fromName(self._conn, self._pool, instance_id)
+ return None
+
+ @exception.wrap_exception
+ def adopt_instances(self):
+ """ if there are instances already running, adopt them """
+ return defer.succeed(0)
+ instance_names = [self._conn.lookupByID(x).name()
+ for x in self._conn.listDomainsID()]
+ for name in instance_names:
+ try:
+ new_inst = Instance.fromName(self._conn, self._pool, name)
+ new_inst.update_state()
+ except:
+ pass
+ return defer.succeed(len(self._instances))
+
+ @exception.wrap_exception
+ def describe_instances(self):
+ retval = {}
+ for inst in self.instdir.by_node(FLAGS.node_name):
+ retval[inst['instance_id']] = (Instance.fromName(self._conn, self._pool, inst['instance_id']))
+ return retval
+
+ @defer.inlineCallbacks
+ def report_state(self):
+ logging.debug("Reporting State")
+ return
+
+ @exception.wrap_exception
+ def run_instance(self, instance_id, **_kwargs):
+ """ launch a new instance with specified options """
+ logging.debug("Starting instance %s..." % (instance_id))
+ inst = self.instdir.get(instance_id)
+ inst['node_name'] = FLAGS.node_name
+ inst.save()
+ # TODO(vish) check to make sure the availability zone matches
+ new_inst = Instance(self._conn, name=instance_id,
+ pool=self._pool, data=inst)
+ if new_inst.is_running():
+ raise exception.Error("Instance is already running")
+ d = new_inst.spawn()
+ return d
+
+ @exception.wrap_exception
+ def terminate_instance(self, instance_id):
+ """ terminate an instance on this machine """
+ logging.debug("Got told to terminate instance %s" % instance_id)
+ instance = self.get_instance(instance_id)
+ # inst = self.instdir.get(instance_id)
+ if not instance:
+ raise exception.Error(
+ 'trying to terminate unknown instance: %s' % instance_id)
+ d = instance.destroy()
+ # d.addCallback(lambda x: inst.destroy())
+ return d
+
+ @exception.wrap_exception
+ def reboot_instance(self, instance_id):
+ """ reboot an instance on this server
+ KVM doesn't support reboot, so we terminate and restart """
+ instance = self.get_instance(instance_id)
+ if not instance:
+ raise exception.Error(
+ 'trying to reboot unknown instance: %s' % instance_id)
+ return instance.reboot()
+
+ @defer.inlineCallbacks
+ @exception.wrap_exception
+ def get_console_output(self, instance_id):
+ """ send the console output for an instance """
+ logging.debug("Getting console output for %s" % (instance_id))
+ inst = self.instdir.get(instance_id)
+ instance = self.get_instance(instance_id)
+ if not instance:
+ raise exception.Error(
+ 'trying to get console log for unknown: %s' % instance_id)
+ rv = yield instance.console_output()
+ # TODO(termie): this stuff belongs in the API layer, no need to
+ # munge the data we send to ourselves
+ output = {"InstanceId" : instance_id,
+ "Timestamp" : "2",
+ "output" : base64.b64encode(rv)}
+ defer.returnValue(output)
+
+ @defer.inlineCallbacks
+ @exception.wrap_exception
+ def attach_volume(self, instance_id = None,
+ aoe_device = None, mountpoint = None):
+ utils.runthis("Attached Volume: %s",
+ "sudo virsh attach-disk %s /dev/etherd/%s %s"
+ % (instance_id, aoe_device, mountpoint.split("/")[-1]))
+ return defer.succeed(True)
+
+ def _init_aoe(self):
+ utils.runthis("Doin an AoE discover, returns %s", "sudo aoe-discover")
+ utils.runthis("Doin an AoE stat, returns %s", "sudo aoe-stat")
+
+ @exception.wrap_exception
+ def detach_volume(self, instance_id, mountpoint):
+ """ detach a volume from an instance """
+ # despite the documentation, virsh detach-disk just wants the device
+ # name without the leading /dev/
+ target = mountpoint.rpartition('/dev/')[2]
+ utils.runthis("Detached Volume: %s", "sudo virsh detach-disk %s %s "
+ % (instance_id, target))
+ return defer.succeed(True)
+
+
+class Group(object):
+ def __init__(self, group_id):
+ self.group_id = group_id
+
+
+class ProductCode(object):
+ def __init__(self, product_code):
+ self.product_code = product_code
+
+
+def _create_image(data, libvirt_xml):
+ """ create libvirt.xml and copy files into instance path """
+ def basepath(path=''):
+ return os.path.abspath(os.path.join(data['basepath'], path))
+
+ def imagepath(path=''):
+ return os.path.join(FLAGS.images_path, path)
+
+ def image_url(path):
+ return "%s:%s/_images/%s" % (FLAGS.s3_host, FLAGS.s3_port, path)
+
+ logging.info(basepath('disk'))
+ try:
+ os.makedirs(data['basepath'])
+ os.chmod(data['basepath'], 0777)
+ except OSError:
+ # TODO: there is already an instance with this name, do something
+ pass
+ try:
+ logging.info('Creating image for: %s', data['instance_id'])
+ f = open(basepath('libvirt.xml'), 'w')
+ f.write(libvirt_xml)
+ f.close()
+ if not FLAGS.fake_libvirt:
+ if FLAGS.use_s3:
+ if not os.path.exists(basepath('disk')):
+ utils.fetchfile(image_url("%s/image" % data['image_id']),
+ basepath('disk-raw'))
+ if not os.path.exists(basepath('kernel')):
+ utils.fetchfile(image_url("%s/image" % data['kernel_id']),
+ basepath('kernel'))
+ if not os.path.exists(basepath('ramdisk')):
+ utils.fetchfile(image_url("%s/image" % data['ramdisk_id']),
+ basepath('ramdisk'))
+ else:
+ if not os.path.exists(basepath('disk')):
+ shutil.copyfile(imagepath("%s/image" % data['image_id']),
+ basepath('disk-raw'))
+ if not os.path.exists(basepath('kernel')):
+ shutil.copyfile(imagepath("%s/image" % data['kernel_id']),
+ basepath('kernel'))
+ if not os.path.exists(basepath('ramdisk')):
+ shutil.copyfile(imagepath("%s/image" %
+ data['ramdisk_id']),
+ basepath('ramdisk'))
+ if data['key_data']:
+ logging.info('Injecting key data into image %s' %
+ data['image_id'])
+ disk.inject_key(data['key_data'], basepath('disk-raw'))
+ if os.path.exists(basepath('disk')):
+ os.remove(basepath('disk'))
+ bytes = INSTANCE_TYPES[data['instance_type']]['local_gb'] * 1024 * 1024 * 1024
+ disk.partition(basepath('disk-raw'), basepath('disk'), bytes)
+ logging.info('Done create image for: %s', data['instance_id'])
+ except Exception as ex:
+ return {'exception': ex}
+
+
+class Instance(object):
+
+ NOSTATE = 0x00
+ RUNNING = 0x01
+ BLOCKED = 0x02
+ PAUSED = 0x03
+ SHUTDOWN = 0x04
+ SHUTOFF = 0x05
+ CRASHED = 0x06
+
+ def is_pending(self):
+ return (self.state == Instance.NOSTATE or self.state == 'pending')
+
+ def is_destroyed(self):
+ return self.state == Instance.SHUTOFF
+
+ def is_running(self):
+ logging.debug("Instance state is: %s" % self.state)
+ return (self.state == Instance.RUNNING or self.state == 'running')
+
+ def __init__(self, conn, pool, name, data):
+ # TODO(termie): pool should probably be a singleton instead of being passed
+ # here and in the classmethods
+ """ spawn an instance with a given name """
+ # TODO(termie): pool should probably be a singleton instead of being passed
+ # here and in the classmethods
+ self._pool = pool
+ self._conn = conn
+ self.datamodel = data
+ print data
+
+ # NOTE(termie): to be passed to multiprocess self._s must be
+ # pickle-able by cPickle
+ self._s = {}
+
+ # TODO(termie): is instance_type that actual name for this?
+ size = data.get('instance_type', FLAGS.default_instance_type)
+ if size not in INSTANCE_TYPES:
+ raise exception.Error('invalid instance type: %s' % size)
+
+ self._s.update(INSTANCE_TYPES[size])
+
+ self._s['name'] = name
+ self._s['instance_id'] = name
+ self._s['instance_type'] = size
+ self._s['mac_address'] = data.get(
+ 'mac_address', 'df:df:df:df:df:df')
+ self._s['basepath'] = data.get(
+ 'basepath', os.path.abspath(
+ os.path.join(FLAGS.instances_path, self.name)))
+ 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['kernel_id'] = data.get('kernel_id', FLAGS.default_kernel)
+ self._s['ramdisk_id'] = data.get('ramdisk_id', FLAGS.default_ramdisk)
+ self._s['owner_id'] = data.get('owner_id', '')
+ self._s['node_name'] = data.get('node_name', '')
+ self._s['user_data'] = data.get('user_data', '')
+ self._s['ami_launch_index'] = data.get('ami_launch_index', None)
+ self._s['launch_time'] = data.get('launch_time', None)
+ self._s['reservation_id'] = data.get('reservation_id', None)
+ # self._s['state'] = Instance.NOSTATE
+ self._s['state'] = data.get('state', Instance.NOSTATE)
+
+ self._s['key_data'] = data.get('key_data', None)
+
+ # TODO: we may not need to save the next few
+ self._s['groups'] = data.get('security_group', ['default'])
+ self._s['product_codes'] = data.get('product_code', [])
+ self._s['key_name'] = data.get('key_name', None)
+ self._s['addressing_type'] = data.get('addressing_type', None)
+ self._s['availability_zone'] = data.get('availability_zone', 'fixme')
+
+ #TODO: put real dns items here
+ self._s['private_dns_name'] = data.get('private_dns_name', 'fixme')
+ self._s['dns_name'] = data.get('dns_name',
+ self._s['private_dns_name'])
+ logging.debug("Finished init of Instance with id of %s" % name)
+
+ def toXml(self):
+ # TODO(termie): cache?
+ logging.debug("Starting the toXML method")
+ libvirt_xml = open(FLAGS.libvirt_xml_template).read()
+ xml_info = self._s.copy()
+ #xml_info.update(self._s)
+
+ # TODO(termie): lazy lazy hack because xml is annoying
+ xml_info['nova'] = json.dumps(self._s)
+ libvirt_xml = libvirt_xml % xml_info
+ logging.debug("Finished the toXML method")
+
+ return libvirt_xml
+
+ @classmethod
+ def fromName(cls, conn, pool, name):
+ """ use the saved data for reloading the instance """
+ # if FLAGS.fake_libvirt:
+ # raise Exception('this is a bit useless, eh?')
+
+ instdir = model.InstanceDirectory()
+ instance = instdir.get(name)
+ return cls(conn=conn, pool=pool, name=name, data=instance)
+
+ @property
+ def state(self):
+ return self._s['state']
+
+ @property
+ def name(self):
+ return self._s['name']
+
+ def describe(self):
+ return self._s
+
+ def info(self):
+ logging.debug("Getting info for dom %s" % self.name)
+ virt_dom = self._conn.lookupByName(self.name)
+ (state, max_mem, mem, num_cpu, cpu_time) = virt_dom.info()
+ return {'state': state,
+ 'max_mem': max_mem,
+ 'mem': mem,
+ 'num_cpu': num_cpu,
+ 'cpu_time': cpu_time}
+
+ def update_state(self):
+ info = self.info()
+ self._s['state'] = info['state']
+ self.datamodel['state'] = info['state']
+ self.datamodel['node_name'] = FLAGS.node_name
+ self.datamodel.save()
+
+ @exception.wrap_exception
+ def destroy(self):
+ if self.is_destroyed():
+ self.datamodel.destroy()
+ raise exception.Error('trying to destroy already destroyed'
+ ' instance: %s' % self.name)
+
+ self._s['state'] = Instance.SHUTDOWN
+ self.datamodel['state'] = 'shutting_down'
+ self.datamodel.save()
+ try:
+ virt_dom = self._conn.lookupByName(self.name)
+ virt_dom.destroy()
+ except Exception, _err:
+ pass
+ # If the instance is already terminated, we're still happy
+ d = defer.Deferred()
+ d.addCallback(lambda x: self.datamodel.destroy())
+ # TODO(termie): short-circuit me for tests
+ # WE'LL save this for when we do shutdown,
+ # instead of destroy - but destroy returns immediately
+ timer = task.LoopingCall(f=None)
+ def _wait_for_shutdown():
+ try:
+ info = self.info()
+ if info['state'] == Instance.SHUTDOWN:
+ self._s['state'] = Instance.SHUTDOWN
+ #self.datamodel['state'] = 'shutdown'
+ #self.datamodel.save()
+ timer.stop()
+ d.callback(None)
+ except Exception:
+ self._s['state'] = Instance.SHUTDOWN
+ timer.stop()
+ d.callback(None)
+ timer.f = _wait_for_shutdown
+ timer.start(interval=0.5, now=True)
+ return d
+
+ @defer.inlineCallbacks
+ @exception.wrap_exception
+ def reboot(self):
+ # if not self.is_running():
+ # raise exception.Error(
+ # 'trying to reboot a non-running'
+ # 'instance: %s (state: %s)' % (self.name, self.state))
+
+ yield self._conn.lookupByName(self.name).destroy()
+ self.datamodel['state'] = 'rebooting'
+ self.datamodel.save()
+ self._s['state'] = Instance.NOSTATE
+ self._conn.createXML(self.toXml(), 0)
+ # TODO(termie): this should actually register a callback to check
+ # for successful boot
+ self.datamodel['state'] = 'running'
+ self.datamodel.save()
+ self._s['state'] = Instance.RUNNING
+ logging.debug('rebooted instance %s' % self.name)
+ defer.returnValue(None)
+
+ @exception.wrap_exception
+ def spawn(self):
+ self.datamodel['state'] = "spawning"
+ self.datamodel.save()
+ logging.debug("Starting spawn in Instance")
+ xml = self.toXml()
+ def _launch(retvals):
+ self.datamodel['state'] = 'launching'
+ self.datamodel.save()
+ try:
+ logging.debug("Arrived in _launch")
+ if retvals and 'exception' in retvals:
+ raise retvals['exception']
+ self._conn.createXML(self.toXml(), 0)
+ # TODO(termie): this should actually register
+ # a callback to check for successful boot
+ self._s['state'] = Instance.RUNNING
+ self.datamodel['state'] = 'running'
+ self.datamodel.save()
+ logging.debug("Instance is running")
+ except Exception as ex:
+ logging.debug(ex)
+ self.datamodel['state'] = 'shutdown'
+ self.datamodel.save()
+ #return self
+
+ d = self._pool.apply(_create_image, self._s, xml)
+ d.addCallback(_launch)
+ return d
+
+ @exception.wrap_exception
+ def console_output(self):
+ if not FLAGS.fake_libvirt:
+ fname = os.path.abspath(
+ os.path.join(self._s['basepath'], 'console.log'))
+ with open(fname, 'r') as f:
+ console = f.read()
+ else:
+ console = 'FAKE CONSOLE OUTPUT'
+ 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)
+