diff options
| author | Jesse Andrews <anotherjesse@gmail.com> | 2010-05-27 23:05:26 -0700 |
|---|---|---|
| committer | Jesse Andrews <anotherjesse@gmail.com> | 2010-05-27 23:05:26 -0700 |
| commit | bf6e6e718cdc7488e2da87b21e258ccc065fe499 (patch) | |
| tree | 51cf4f72047eb6b16079c7fe21e9822895541801 /nova/compute | |
initial commit
Diffstat (limited to 'nova/compute')
| -rw-r--r-- | nova/compute/__init__.py | 28 | ||||
| -rw-r--r-- | nova/compute/disk.py | 122 | ||||
| -rw-r--r-- | nova/compute/exception.py | 35 | ||||
| -rw-r--r-- | nova/compute/fakevirtinstance.xml | 43 | ||||
| -rw-r--r-- | nova/compute/libvirt.xml.template | 46 | ||||
| -rw-r--r-- | nova/compute/linux_net.py | 146 | ||||
| -rw-r--r-- | nova/compute/model.py | 203 | ||||
| -rw-r--r-- | nova/compute/network.py | 520 | ||||
| -rw-r--r-- | nova/compute/node.py | 549 |
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) + |
