Source code for maasserver.models.nodegroup

# Copyright 2012-2014 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Model definition for NodeGroup which models a collection of Nodes."""

from __future__ import (
    absolute_import,
    print_function,
    unicode_literals,
    )

str = None

__metaclass__ = type
__all__ = [
    'NodeGroup',
    'NODEGROUP_CLUSTER_NAME_TEMPLATE',
    ]


from django.db.models import (
    CharField,
    ForeignKey,
    IntegerField,
    Manager,
    )
from maasserver import DefaultMeta
from maasserver.enum import (
    NODEGROUP_STATUS,
    NODEGROUP_STATUS_CHOICES,
    NODEGROUPINTERFACE_MANAGEMENT,
    )
from maasserver.models.bootsource import BootSource
from maasserver.models.bootsourceselection import BootSourceSelection
from maasserver.models.nodegroupinterface import NodeGroupInterface
from maasserver.models.timestampedmodel import TimestampedModel
from maasserver.refresh_worker import refresh_worker
from piston.models import (
    KEY_SIZE,
    Token,
    )
from provisioningserver.omshell import generate_omapi_key
from provisioningserver.tasks import (
    add_new_dhcp_host_map,
    add_seamicro15k,
    add_virsh,
    enlist_nodes_from_ucsm,
    import_boot_images,
    report_boot_images,
    )


class NodeGroupManager(Manager):
    """Manager for the NodeGroup class.

    Don't import or instantiate this directly; access as `<Class>.objects` on
    the model class it manages.
    """

    def new(self, name, uuid, ip, subnet_mask=None,
            broadcast_ip=None, router_ip=None, ip_range_low=None,
            ip_range_high=None, dhcp_key='', interface='',
            status=NODEGROUP_STATUS.DEFAULT,
            management=NODEGROUPINTERFACE_MANAGEMENT.DEFAULT,
            cluster_name=None, maas_url=''):
        """Create a :class:`NodeGroup` with the given parameters.

        This method will:
        - create the related NodeGroupInterface if `interface` is provided
        - generate API credentials for the nodegroup's worker to use.
        """
        dhcp_values = [
            interface,
            subnet_mask,
            router_ip,
            ip_range_low,
            ip_range_high,
            ]
        assert all(dhcp_values) or not any(dhcp_values), (
            "Provide all DHCP settings, or none at all. "
            "Only the broadcast address is optional.")

        if cluster_name is None:
            cluster_name = NODEGROUP_CLUSTER_NAME_TEMPLATE % {'uuid': uuid}
        nodegroup = NodeGroup(
            name=name, uuid=uuid, cluster_name=cluster_name, dhcp_key=dhcp_key,
            status=status, maas_url=maas_url)
        nodegroup.save()
        if interface != '':
            nginterface = NodeGroupInterface(
                nodegroup=nodegroup, ip=ip, subnet_mask=subnet_mask,
                broadcast_ip=broadcast_ip, router_ip=router_ip,
                interface=interface, ip_range_low=ip_range_low,
                ip_range_high=ip_range_high, management=management)
            nginterface.save()
        return nodegroup

    def ensure_master(self):
        """Obtain the master node group, creating it first if needed."""
        # Avoid circular imports.
        from maasserver.models import Node
        from maasserver.forms import DEFAULT_DNS_ZONE_NAME

        try:
            # Get the first created nodegroup if it exists.
            master = self.all().order_by('id')[0:1].get()
        except NodeGroup.DoesNotExist:
            # The master did not exist yet; create it on demand.
            master = self.new(
                DEFAULT_DNS_ZONE_NAME, 'master', '127.0.0.1',
                dhcp_key=generate_omapi_key(),
                status=NODEGROUP_STATUS.ACCEPTED)

            # If any legacy nodes were still not associated with a node
            # group, enroll them in the master node group.
            Node.objects.filter(nodegroup=None).update(nodegroup=master)

        return master

    def get_by_natural_key(self, uuid):
        """For Django, a node group's uuid is a natural key."""
        return self.get(uuid=uuid)

    def refresh_workers(self):
        """Send refresh tasks to all node-group workers."""
        for nodegroup in self.filter(status=NODEGROUP_STATUS.ACCEPTED):
            refresh_worker(nodegroup)

    def _mass_change_status(self, old_status, new_status):
        nodegroups = self.filter(status=old_status)
        nodegroups_count = nodegroups.count()
        # Change the nodegroups one by one in order to trigger the
        # post_save signals.
        for nodegroup in nodegroups:
            nodegroup.status = new_status
            nodegroup.save()
        return nodegroups_count

    def reject_all_pending(self):
        """Change the status of the 'PENDING' nodegroup to 'REJECTED."""
        return self._mass_change_status(
            NODEGROUP_STATUS.PENDING, NODEGROUP_STATUS.REJECTED)

    def accept_all_pending(self):
        """Change the status of the 'PENDING' nodegroup to 'ACCEPTED."""
        return self._mass_change_status(
            NODEGROUP_STATUS.PENDING, NODEGROUP_STATUS.ACCEPTED)

    def import_boot_images_accepted_clusters(self):
        """Import the boot images on all the accepted cluster controllers."""
        accepted_nodegroups = NodeGroup.objects.filter(
            status=NODEGROUP_STATUS.ACCEPTED)
        for nodegroup in accepted_nodegroups:
            nodegroup.import_boot_images()


NODEGROUP_CLUSTER_NAME_TEMPLATE = "Cluster %(uuid)s"


[docs]class NodeGroup(TimestampedModel): class Meta(DefaultMeta): """Needed for South to recognize this model.""" objects = NodeGroupManager() cluster_name = CharField( max_length=100, unique=True, editable=True, blank=True, null=False) # A node group's name is also used for the group's DNS zone. name = CharField( max_length=80, unique=False, editable=True, blank=True, null=False) status = IntegerField( choices=NODEGROUP_STATUS_CHOICES, editable=True, default=NODEGROUP_STATUS.DEFAULT) # Credentials for the worker to access the API with. api_token = ForeignKey(Token, null=False, editable=False, unique=True) api_key = CharField( max_length=KEY_SIZE, null=False, blank=False, editable=False, unique=True) dhcp_key = CharField( blank=True, editable=False, max_length=255, default='') # Unique identifier of the worker. uuid = CharField( max_length=36, unique=True, null=False, blank=False, editable=True) # The URL where the cluster controller can access the region # controller. maas_url = CharField( blank=True, editable=False, max_length=255, default='') def __repr__(self): return "<NodeGroup %s>" % self.uuid
[docs] def accept(self): """Accept this nodegroup's enlistment.""" self.status = NODEGROUP_STATUS.ACCEPTED self.save()
[docs] def reject(self): """Reject this nodegroup's enlistment.""" self.status = NODEGROUP_STATUS.REJECTED self.save()
def save(self, *args, **kwargs): if self.api_token_id is None: # Avoid circular imports. from maasserver.models.user import create_auth_token from maasserver.worker_user import get_worker_user api_token = create_auth_token(get_worker_user()) self.api_token = api_token self.api_key = api_token.key return super(NodeGroup, self).save(*args, **kwargs)
[docs] def get_managed_interfaces(self): """Return the list of interfaces for which MAAS manages DHCP.""" # Filter in python instead of in SQL. This will use the cached # version of self.nodegroupinterface_set if present. return [ itf for itf in self.nodegroupinterface_set.all() if itf.management != NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED ]
[docs] def ensure_boot_source_definition(self): """Set default boot source if none is currently defined.""" if not self.bootsource_set.exists(): source = BootSource.objects.create( cluster=self, url='http://maas.ubuntu.com/images/ephemeral-v2/releases/', keyring_filename=( '/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg')) # Default is to import supported Ubuntu LTS releases, for all # architectures, release versions only. for os_release in ('precise', 'trusty'): BootSourceSelection.objects.create( boot_source=source, release=os_release, arches=['*'], subarches=['*'], labels=['release'])
[docs] def manages_dns(self): """Does this `NodeGroup` manage DNS on any interfaces? This returns `True` when the `NodeGroup` is accepted, and has a `NodeGroupInterface` that's set to manage both DHCP and DNS. """ if self.status != NODEGROUP_STATUS.ACCEPTED: return False # Filter in python instead of in SQL. This will use the cached # version of self.nodegroupinterface_set if present. for itf in self.nodegroupinterface_set.all(): if itf.management == NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS: return True return False
[docs] def ensure_dhcp_key(self): """Ensure that this nodegroup has a dhcp key. This method persists the dhcp key without triggering the model signals (pre_save/post_save/etc) because it's called from dhcp.configure_dhcp which, in turn, it called from the post_save signal of NodeGroup.""" if self.dhcp_key == '': dhcp_key = generate_omapi_key() self.dhcp_key = dhcp_key # Persist the dhcp_key without triggering the signals. NodeGroup.objects.filter(id=self.id).update(dhcp_key=dhcp_key)
@property
[docs] def work_queue(self): """The name of the queue for tasks specific to this nodegroup.""" return self.uuid
[docs] def import_boot_images(self): """Import the pxe files on this cluster controller. The files are downloaded through the proxy defined in the config setting 'http_proxy' if defined. """ # Avoid circular imports. from maasserver.models import Config task_kwargs = { 'callback': report_boot_images.subtask( options={'queue': self.uuid}), } http_proxy = Config.objects.get_config('http_proxy') if http_proxy is not None: task_kwargs['http_proxy'] = http_proxy import_boot_images.apply_async(queue=self.uuid, kwargs=task_kwargs)
[docs] def add_seamicro15k(self, mac, username, password, power_control=None): """ Add all of the specified cards the Seamicro SM15000 chassis at the specified MAC. :param mac: MAC address of the card. :param username: username for power controller :param password: password for power controller :param power_control: optional specify the power control method, either ipmi (default), restapi, or restapi2. """ args = (mac, username, password, power_control) add_seamicro15k.apply_async(queue=self.uuid, args=args)
[docs] def add_virsh(self, poweraddr, password=None): """ Add all of the virtual machines inside a virsh controller. :param poweraddr: virsh connection string :param password: ssh password """ args = (poweraddr, password) add_virsh.apply_async(queue=self.uuid, args=args)
[docs] def enlist_nodes_from_ucsm(self, url, username, password): """ Add the servers from a Cicso UCS Manager. :param URL: URL of the Cisco UCS Manager HTTP-XML API. :param username: username for UCS Manager. :param password: password for UCS Manager. """ args = (url, username, password) enlist_nodes_from_ucsm.apply_async(queue=self.uuid, args=args)
def add_dhcp_host_maps(self, new_leases): if len(new_leases) > 0 and len(self.get_managed_interfaces()) > 0: # XXX JeroenVermeulen 2012-08-21, bug=1039362: the DHCP # server is currently always local to the worker system, so # use 127.0.0.1 as the DHCP server address. task_kwargs = dict( mappings=new_leases, server_address='127.0.0.1', shared_key=self.dhcp_key) add_new_dhcp_host_map.apply_async( queue=self.uuid, kwargs=task_kwargs)
MAAS logo

MAAS

Metal As A Service.



Related Topics