예제 #1
0
    def __init__(self, name, chefapi, cloudapi, field_handlers, log, options):
        """Parses the droplet's config file section in trac.ini.  Here's
        an example trac.ini droplet section:
        
          [cloud.instance]
          class = Ec2Instance
          title = EC2 Instances
          order = 1
          label = Instance
          description = AWS EC2 instances.
        
        The 'instance' in '[cloud.instance]' is the droplet name which is
        used to uniquely identify the class of cloud resources including in
        the trac url structure (e.g., /cloud/instance).
        
        The 'class' value must exactly match the python class name for the
        corresponding droplet.
        
        The 'title' and 'order' options are used for contextual navigation.
        The 'order' should start with 1.  If 'order' is omitted then the
        droplet will not be returned as a contextual navigation element
        (but would still be accessible by its url). The 'label' value is
        displayed in various buttons and forms for the name of a single
        droplet item.  The 'description' option is displayed in the grid
        view (much like a report description).
        
        The remaining fields (not shown above) are for querying chef and
        are parsed by the Fields class.
        """
        self.name = name
        self.chefapi = chefapi
        self.cloudapi = cloudapi
        self.log = log
        self.log.debug('Instantiating droplet %s' % name)

        prefix = 'field.'
        self.fields = Fields(options, field_handlers, chefapi, cloudapi, log,
                             prefix)

        for k, v in options.items():
            if k.startswith(prefix):
                continue  # handled by Fields class above
            if k in ('crud_view', 'crud_new', 'crud_edit', 'grid_columns'):
                self.fields.set_list(k, v)
                continue
            setattr(self, k, v)
        self.log.info('Instantiated droplet %s' % name)
예제 #2
0
 def __init__(self, name, chefapi, cloudapi, field_handlers, log, options):
     """Parses the droplet's config file section in trac.ini.  Here's
     an example trac.ini droplet section:
     
       [cloud.instance]
       class = Ec2Instance
       title = EC2 Instances
       order = 1
       label = Instance
       description = AWS EC2 instances.
     
     The 'instance' in '[cloud.instance]' is the droplet name which is
     used to uniquely identify the class of cloud resources including in
     the trac url structure (e.g., /cloud/instance).
     
     The 'class' value must exactly match the python class name for the
     corresponding droplet.
     
     The 'title' and 'order' options are used for contextual navigation.
     The 'order' should start with 1.  If 'order' is omitted then the
     droplet will not be returned as a contextual navigation element
     (but would still be accessible by its url). The 'label' value is
     displayed in various buttons and forms for the name of a single
     droplet item.  The 'description' option is displayed in the grid
     view (much like a report description).
     
     The remaining fields (not shown above) are for querying chef and
     are parsed by the Fields class.
     """
     self.name = name
     self.chefapi = chefapi
     self.cloudapi = cloudapi
     self.log = log
     self.log.debug('Instantiating droplet %s' % name)
     
     prefix = 'field.'
     self.fields = Fields(options, field_handlers, chefapi, cloudapi,
                          log, prefix)
     
     for k,v in options.items():
         if k.startswith(prefix):
             continue # handled by Fields class above
         if k in ('crud_view','crud_new','crud_edit','grid_columns'):
             self.fields.set_list(k,v)
             continue
         setattr(self, k, v)
     self.log.info('Instantiated droplet %s' % name)
예제 #3
0
class Droplet(object):
    """Generic class for a cloud resource that I'm calling a 'droplet'
    to distinguish it from Trac resources.  Droplets are controllers
    that coordinate trac, chef, and cloud behaviors."""
    @classmethod
    def new(cls, env, name, chefapi, cloudapi, field_handlers, log):
        """Return a new droplet instance based on the class name defined
        in the droplet's trac.ini config section - e.g.,
        
          [cloud.instance]
          class = Ec2Instance
        
        where the name param = 'instance'.  Default options for the
        droplet are overridden by those in the trac.ini file.
        """
        from cloud.droplet.ec2_instance import Ec2Instance  #@UnusedImport
        from cloud.droplet.rds_instance import RdsInstance  #@UnusedImport
        from cloud.droplet.ebs_volume import EbsVolume  #@UnusedImport
        from cloud.droplet.eip_address import EipAddress  #@UnusedImport
        from cloud.droplet.command import Command  #@UnusedImport
        from cloud.droplet.environment import Environment  #@UnusedImport

        options = cls.options(env).get(name)
        cls = locals()[options['class']]
        return cls(name, chefapi, cloudapi, field_handlers, log, options)

    @classmethod
    def titles(cls, env):
        """Return a list of tuples of order, droplet name, and its title."""
        titles = []
        for droplet_name, opts in cls.options(env).items():
            order = int(opts.get('order', 99))
            title = opts.get('title', droplet_name)
            titles.append((order, droplet_name, title))
        return sorted(titles)

    @classmethod
    def options(cls, env):
        """Return a dict of all droplet options - keys are droplet names
        and values are its config options created by merging the main
        [cloud] section with the defaults overridden by its respective
        trac.ini section."""
        options = {}
        for section in set(droplet_defaults.keys() + env.config.sections()):
            if not section.startswith('cloud.'):
                continue
            opts = copy.copy(dict(env.config.options('cloud')))  # main section
            opts['trac_base_url'] = env.config.get('trac', 'base_url')
            opts.update(droplet_defaults.get(section, {}))  # defaults
            opts.update(env.config.options(section))  # overrides
            options[section.replace('cloud.', '', 1)] = opts
        return options

    def __init__(self, name, chefapi, cloudapi, field_handlers, log, options):
        """Parses the droplet's config file section in trac.ini.  Here's
        an example trac.ini droplet section:
        
          [cloud.instance]
          class = Ec2Instance
          title = EC2 Instances
          order = 1
          label = Instance
          description = AWS EC2 instances.
        
        The 'instance' in '[cloud.instance]' is the droplet name which is
        used to uniquely identify the class of cloud resources including in
        the trac url structure (e.g., /cloud/instance).
        
        The 'class' value must exactly match the python class name for the
        corresponding droplet.
        
        The 'title' and 'order' options are used for contextual navigation.
        The 'order' should start with 1.  If 'order' is omitted then the
        droplet will not be returned as a contextual navigation element
        (but would still be accessible by its url). The 'label' value is
        displayed in various buttons and forms for the name of a single
        droplet item.  The 'description' option is displayed in the grid
        view (much like a report description).
        
        The remaining fields (not shown above) are for querying chef and
        are parsed by the Fields class.
        """
        self.name = name
        self.chefapi = chefapi
        self.cloudapi = cloudapi
        self.log = log
        self.log.debug('Instantiating droplet %s' % name)

        prefix = 'field.'
        self.fields = Fields(options, field_handlers, chefapi, cloudapi, log,
                             prefix)

        for k, v in options.items():
            if k.startswith(prefix):
                continue  # handled by Fields class above
            if k in ('crud_view', 'crud_new', 'crud_edit', 'grid_columns'):
                self.fields.set_list(k, v)
                continue
            setattr(self, k, v)
        self.log.info('Instantiated droplet %s' % name)

    def render_grid(self, req):
        """Retrieve the droplets and pre-process them for rendering."""
        self.log.debug('Rendering grid..')
        index = self.grid_index
        columns = self.fields.get_list('grid_columns')

        format = req.args.get('format')
        resource = Resource('cloud', self.name)
        context = Context.from_request(req, resource)

        page = int(req.args.get('page', '1'))
        default_max = {
            'rss': self.items_per_page_rss,
            'csv': 0,
            'tab': 0
        }.get(format, self.items_per_page)
        max = req.args.get('max')
        query = req.args.get('query')
        groupby = req.args.get('groupby', self.grid_group)
        groupby_fields = [(field.label, field.name) for field in columns]
        limit = as_int(max, default_max,
                       min=0)  # explicit max takes precedence
        offset = (page - 1) * limit

        # explicit sort takes precedence over config
        sort = groupby or req.args.get('sort', self.grid_sort)
        asc = req.args.get('asc', self.grid_asc)
        asc = bool(int(asc))  # string '0' or '1' to int/boolean

        def droplet_href(**kwargs):
            """Generate links to this cloud droplet preserving user
            variables, and sorting and paging variables.
            """
            params = {}
            if sort:
                params['sort'] = sort
            params['page'] = page
            if max:
                params['max'] = max
            if query:
                params['query'] = query
            if groupby:
                params['groupby'] = groupby
            params.update(kwargs)
            params['asc'] = params.get('asc', asc) and '1' or '0'
            return req.href.cloud(self.name, params)

        data = {
            'action': 'view',
            'buttons': [],
            'resource': resource,
            'context': context,
            'title': self.title,
            'description': self.description,
            'label': self.label,
            'columns': columns,
            'id_field': self.id_field,
            'max': limit,
            'query': query,
            'groupby': groupby,
            'groupby_fields': [('', '')] + groupby_fields,
            'message': None,
            'paginator': None,
            'droplet_href': droplet_href,
        }

        try:
            self.log.debug('About to search chef..')
            sort_ = sort.strip('_')  # handle dynamic attributes
            rows, total = self.chefapi.search(index, sort_, asc, limit, offset,
                                              query or '*:*')
            numrows = len(rows)
            self.log.debug('Chef search returned %s rows' % numrows)
        except Exception:
            import traceback
            msg = "Oops...\n" + traceback.format_exc() + "\n"
            data['message'] = _(to_unicode(msg))
            self.log.debug(data['message'])
            return 'droplet_grid.html', data, None

        paginator = None
        if limit > 0:
            paginator = Paginator(rows, page - 1, limit, total)
            data['paginator'] = paginator
            if paginator.has_next_page:
                add_link(req, 'next', droplet_href(page=page + 1),
                         _('Next Page'))
            if paginator.has_previous_page:
                add_link(req, 'prev', droplet_href(page=page - 1),
                         _('Previous Page'))

            pagedata = []
            shown_pages = paginator.get_shown_pages(21)
            for p in shown_pages:
                pagedata.append([
                    droplet_href(page=p), None,
                    str(p),
                    _('Page %(num)d', num=p)
                ])
            fields = ['href', 'class', 'string', 'title']
            paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata]
            paginator.current_page = {
                'href': None,
                'class': 'current',
                'string': str(paginator.page + 1),
                'title': None
            }
            numrows = paginator.num_items

        # Place retrieved columns in groups, according to naming conventions
        #  * _col_ means fullrow, i.e. a group with one header
        #  * col_ means finish the current group and start a new one

        header_groups = [[]]
        for field in columns:
            header = {
                'col': field.name,
                'title': field.label,
                'hidden': False,
                'asc': None,
            }

            if field.name == sort:
                header['asc'] = asc

            header_group = header_groups[-1]
            header_group.append(header)

        # Structure the rows and cells:
        #  - group rows according to __group__ value, if defined
        #  - group cells the same way headers are grouped
        row_groups = []
        authorized_results = []
        prev_group_value = None
        for row_idx, item in enumerate(rows):
            col_idx = 0
            cell_groups = []
            row = {'cell_groups': cell_groups}
            for header_group in header_groups:
                cell_group = []
                for header in header_group:
                    col = header['col']
                    field = self.fields[col]
                    value = field.get(item, req)
                    cell = {'value': value, 'header': header, 'index': col_idx}
                    col_idx += 1
                    # Detect and create new group
                    if col == groupby and value != prev_group_value:
                        prev_group_value = value
                        row_groups.append((value, []))
                    # Other row properties
                    row['__idx__'] = row_idx
                    if col == self.id_field:
                        row['id'] = value
                    cell_group.append(cell)
                cell_groups.append(cell_group)
            resource = Resource('cloud', '%s/%s' % (self.name, row['id']))
            if 'CLOUD_VIEW' not in req.perm(resource):
                continue
            authorized_results.append(item)
            row['resource'] = resource
            if row_groups:
                row_group = row_groups[-1][1]
            else:
                row_group = []
                row_groups = [(None, row_group)]
            row_group.append(row)

        data.update({
            'header_groups': header_groups,
            'row_groups': row_groups,
            'numrows': numrows,
            'sorting_enabled': len(row_groups) == 1
        })
        # FIXME: implement formats
        #        if format == 'rss':
        #            data['email_map'] = Chrome(self.env).get_email_map()
        #            data['context'] = Context.from_request(req, report_resource,
        #                                                   absurls=True)
        #            return 'report.rss', data, 'application/rss+xml'
        #        elif format == 'csv':
        #            filename = id and 'report_%s.csv' % id or 'report.csv'
        #            self._send_csv(req, cols, authorized_results, mimetype='text/csv',
        #                           filename=filename)
        #        elif format == 'tab':
        #            filename = id and 'report_%s.tsv' % id or 'report.tsv'
        #            self._send_csv(req, cols, authorized_results, '\t',
        #                           mimetype='text/tab-separated-values',
        #                           filename=filename)
        #        else:
        page = max is not None and page or None
        add_link(req, 'alternate', droplet_href(format='rss', page=None),
                 _('RSS Feed'), 'application/rss+xml', 'rss')
        add_link(req, 'alternate', droplet_href(format='csv', page=page),
                 _('Comma-delimited Text'), 'text/plain')
        add_link(req, 'alternate', droplet_href(format='tab', page=page),
                 _('Tab-delimited Text'), 'text/plain')

        self.log.debug('Rendered grid')
        return 'droplet_grid.html', data, None

    def render_view(self, req, id):
        self.log.debug('Rendering view..')
        req.perm.require('CLOUD_VIEW')
        item = self.chefapi.resource(self.crud_resource, id, self.name)

        data = {
            'title': _('%(label)s %(id)s', label=self.label, id=id),
            'buttons': [],
            'action': 'view',
            'droplet_name': self.name,
            'id': id,
            'label': self.label,
            'fields': self.fields.get_list('crud_view', filter=r"cmd_.*"),
            'cmd_fields': self.fields.get_list('crud_view',
                                               filter=r"(?!cmd_)"),
            'item': item,
            'req': req,
            'message': req.args.get('message')
        }

        self.log.debug('Rendered view')
        return 'droplet_view.html', data, None

    def render_edit(self, req, id):
        self.log.debug('Rendering edit/new..')
        if id:
            item = self.chefapi.resource(self.crud_resource, id, self.name)
        else:
            item = None

        data = {
            'droplet_name': self.name,
            'id': id,
            'label': self.label,
            'item': item,
            'req': req,
            'error': req.args.get('error')
        }

        # check if creating or editing
        if id:
            req.perm.require('CLOUD_MODIFY')
            data.update({
                'title':
                _('Edit  %(label)s %(id)s', label=self.label, id=id),
                'buttons': [('save', _('Save %(label)s', label=self.label))],
                'action':
                'edit',
                'fields':
                self.fields.get_list('crud_edit')
            })
        else:
            req.perm.require('CLOUD_CREATE')
            data.update({
                'title':
                _('Create New %(label)s', label=self.label),
                'buttons': [('new', _('Create %(label)s', label=self.label))],
                'action':
                'new',
                'fields':
                self.fields.get_list('crud_new')
            })

        self.log.debug('Rendered edit/new')
        return 'droplet_edit.html', data, None

    def render_delete(self, req, id):
        self.log.debug('Rendering delete..')
        req.perm.require('CLOUD_DELETE')
        data = {
            'title': _('Delete  %(label)s %(id)s', label=self.label, id=id),
            'button': _('Delete %(label)s', label=self.label),
            'droplet_name': self.name,
            'id': id,
            'label': self.label
        }
        self.log.debug('Rendered delete')
        return 'droplet_delete.html', data, None

    def render_progress(self, req, file):
        self.log.debug('Rendering progress..')
        req.perm.require('CLOUD_MODIFY')

        data = {
            'title': self.title,
            'label': self.label,
            'action': 'progress',
            'file': file,
            'droplet_name': self.name,
            'req': req,
            'error': req.args.get('error')
        }

        self.log.debug('Rendered progress')
        return 'droplet_progress.html', data, None

    def create(self, req):
        req.perm.require('CLOUD_CREATE')
        id = req.args.get('name')
        self.log.debug('Creating data bag item %s/%s..' % (self.name, id))
        fields = self.fields.get_list('crud_new', filter=r"cmd_.*")
        self.save(req, id, fields, redirect=False)
        add_notice(
            req,
            _('%(label)s %(id)s has been created.', label=self.label, id=id))
        req.redirect(req.href.cloud(self.name, id))

    def save(self, req, id, fields=None, redirect=True):
        req.perm.require('CLOUD_MODIFY')
        bag = self.chefapi.resource('data', name=self.name)
        item = self.chefapi.databagitem(bag, id)
        fields = fields or self.fields.get_list('crud_edit', filter=r"cmd_.*")
        for field in fields:
            field.set(item, req, default='')
        item.save()
        self.log.info('Saved data bag item %s/%s..' % (bag.name, id))
        if redirect:
            add_notice(
                req,
                _('%(label)s %(id)s has been saved.', label=self.label, id=id))
            req.redirect(req.href.cloud(self.name, id))

    def delete(self, req, id):
        req.perm.require('CLOUD_DELETE')
        self.log.debug('Deleting data bag item %s/%s..' % (self.name, id))
        bag = self.chefapi.resource('data', name=self.name)
        item = self.chefapi.databagitem(bag, id)
        item.delete()
        self.log.info('Deleted data bag item %s/%s' % (self.name, id))
        add_notice(
            req,
            _('%(label)s %(id)s has been deleted.', label=self.label, id=id))
        req.redirect(req.href.cloud(self.name))

    def audit(self, req, id=None, redirect=True):
        req.redirect(req.href.cloud(self.name))

    def _spawn(self, req, exe, launch_data, attributes, progress_file=None):
        """Helper function to spawn processes with progress tracking."""
        if not progress_file:
            progress_file = Progress.get_file()

        # create the command
        cmd = [
            '/usr/bin/python',
            exe,
            '--daemonize',
            '--trac-base-url="%s"' % self.trac_base_url,
            '--progress-file="%s"' % progress_file,
            '--log-file="%s"' % self.log.handlers[0].stream.name,
            '--chef-base-path="%s"' % self.chefapi.base_path,
            '--aws-key="%s"' % self.cloudapi.key,
            '--aws-secret="%s"' % self.cloudapi.secret,
            '--aws-keypair="%s"' % self.cloudapi.keypair,
            '--aws-keypair-pem="%s"' % self.chefapi.keypair_pem,
            '--aws-username="******"' % self.chefapi.user,
            '--aws-security-groups="%s"' % self.cloudapi.security_groups,
            '--rds-username="******"' % self.cloudapi.username,
            '--rds-password="******"' % self.cloudapi.password,
            '--databag="%s"' % self.name,
            "--launch-data='%s'" % json.dumps(launch_data),
            "--attributes='%s'" % json.dumps(attributes),
            '--started-by="%s"' % req.authname,
            '--notify-jabber="%s"' % self.notify_jabber,
            '--jabber-server="%s"' % self.jabber_server,
            '--jabber-port="%s"' % self.jabber_port,
            '--jabber-server="%s"' % self.jabber_server,
            '--jabber-username="******"' % self.jabber_username,
            '--jabber-password="******"' % self.jabber_password,
            '--jabber-channel="%s"' % self.jabber_channel,
        ]
        cmd += ['--chef-boot-run-list="%s"' % \
            r for r in self.chefapi.boot_run_list]
        if self.chefapi.boot_sudo:
            cmd += ['--chef-boot-sudo']
        if self.chefapi.boot_version:
            cmd += ['--chef-boot-version="%s"' % self.chefapi.boot_version]
        cmd = ' '.join(cmd)

        # Spawn command as daemon to launch and bootstrap instance in background
        self.log.debug('Daemonizing command: %s' % cmd)
        if subprocess.call(cmd, shell=True):
            add_warning(req, _("Error daemonizing: %(cmd)s", cmd=cmd))
            req.redirect(req.href.cloud(self.name))
        req.redirect(
            req.href.cloud(self.name, action='progress', file=progress_file))
예제 #4
0
class Droplet(object):
    """Generic class for a cloud resource that I'm calling a 'droplet'
    to distinguish it from Trac resources.  Droplets are controllers
    that coordinate trac, chef, and cloud behaviors."""
    
    @classmethod
    def new(cls, env, name, chefapi, cloudapi, field_handlers, log):
        """Return a new droplet instance based on the class name defined
        in the droplet's trac.ini config section - e.g.,
        
          [cloud.instance]
          class = Ec2Instance
        
        where the name param = 'instance'.  Default options for the
        droplet are overridden by those in the trac.ini file.
        """
        from cloud.droplet.ec2_instance import Ec2Instance #@UnusedImport
        from cloud.droplet.rds_instance import RdsInstance #@UnusedImport
        from cloud.droplet.ebs_volume import EbsVolume #@UnusedImport
        from cloud.droplet.eip_address import EipAddress #@UnusedImport
        from cloud.droplet.command import Command #@UnusedImport
        from cloud.droplet.environment import Environment #@UnusedImport
        
        options = cls.options(env).get(name)
        cls = locals()[options['class']]
        return cls(name, chefapi, cloudapi, field_handlers, log, options)
    
    @classmethod
    def titles(cls, env):
        """Return a list of tuples of order, droplet name, and its title."""
        titles = []
        for droplet_name,opts in cls.options(env).items():
            order = int(opts.get('order',99))
            title = opts.get('title',droplet_name)
            titles.append( (order,droplet_name,title) )
        return sorted(titles)
    
    @classmethod
    def options(cls, env):
        """Return a dict of all droplet options - keys are droplet names
        and values are its config options created by merging the main
        [cloud] section with the defaults overridden by its respective
        trac.ini section."""
        options = {}
        for section in set(droplet_defaults.keys() + env.config.sections()):
            if not section.startswith('cloud.'):
                continue
            opts = copy.copy(dict(env.config.options('cloud'))) # main section
            opts['trac_base_url'] = env.config.get('trac','base_url')
            opts.update(droplet_defaults.get(section,{}))       # defaults
            opts.update(env.config.options(section))            # overrides
            options[section.replace('cloud.','',1)] = opts
        return options
    
    def __init__(self, name, chefapi, cloudapi, field_handlers, log, options):
        """Parses the droplet's config file section in trac.ini.  Here's
        an example trac.ini droplet section:
        
          [cloud.instance]
          class = Ec2Instance
          title = EC2 Instances
          order = 1
          label = Instance
          description = AWS EC2 instances.
        
        The 'instance' in '[cloud.instance]' is the droplet name which is
        used to uniquely identify the class of cloud resources including in
        the trac url structure (e.g., /cloud/instance).
        
        The 'class' value must exactly match the python class name for the
        corresponding droplet.
        
        The 'title' and 'order' options are used for contextual navigation.
        The 'order' should start with 1.  If 'order' is omitted then the
        droplet will not be returned as a contextual navigation element
        (but would still be accessible by its url). The 'label' value is
        displayed in various buttons and forms for the name of a single
        droplet item.  The 'description' option is displayed in the grid
        view (much like a report description).
        
        The remaining fields (not shown above) are for querying chef and
        are parsed by the Fields class.
        """
        self.name = name
        self.chefapi = chefapi
        self.cloudapi = cloudapi
        self.log = log
        self.log.debug('Instantiating droplet %s' % name)
        
        prefix = 'field.'
        self.fields = Fields(options, field_handlers, chefapi, cloudapi,
                             log, prefix)
        
        for k,v in options.items():
            if k.startswith(prefix):
                continue # handled by Fields class above
            if k in ('crud_view','crud_new','crud_edit','grid_columns'):
                self.fields.set_list(k,v)
                continue
            setattr(self, k, v)
        self.log.info('Instantiated droplet %s' % name)
    
    def render_grid(self, req):
        """Retrieve the droplets and pre-process them for rendering."""
        self.log.debug('Rendering grid..')
        index = self.grid_index
        columns = self.fields.get_list('grid_columns')
        
        format = req.args.get('format')
        resource = Resource('cloud', self.name)
        context = Context.from_request(req, resource)

        page = int(req.args.get('page', '1'))
        default_max = {'rss': self.items_per_page_rss,
                       'csv': 0,
                       'tab': 0}.get(format, self.items_per_page)
        max = req.args.get('max')
        query = req.args.get('query')
        groupby = req.args.get('groupby', self.grid_group)
        groupby_fields = [(field.label,field.name) for field in columns]
        limit = as_int(max, default_max, min=0) # explicit max takes precedence
        offset = (page - 1) * limit
        
        # explicit sort takes precedence over config
        sort = groupby or req.args.get('sort', self.grid_sort)
        asc = req.args.get('asc', self.grid_asc)
        asc = bool(int(asc)) # string '0' or '1' to int/boolean
        
        def droplet_href(**kwargs):
            """Generate links to this cloud droplet preserving user
            variables, and sorting and paging variables.
            """
            params = {}
            if sort:
                params['sort'] = sort
            params['page'] = page
            if max:
                params['max'] = max
            if query:
                params['query'] = query
            if groupby:
                params['groupby'] = groupby
            params.update(kwargs)
            params['asc'] = params.get('asc', asc) and '1' or '0'            
            return req.href.cloud(self.name, params)
        
        data = {'action': 'view',
                'buttons': [],
                'resource': resource,
                'context': context,
                'title': self.title,
                'description': self.description,
                'label': self.label,
                'columns': columns,
                'id_field': self.id_field,
                'max': limit,
                'query': query,
                'groupby': groupby,
                'groupby_fields': [('','')] + groupby_fields,
                'message': None,
                'paginator': None,
                'droplet_href': droplet_href,
                }
        
        try:
            self.log.debug('About to search chef..')
            sort_ = sort.strip('_') # handle dynamic attributes
            rows,total = self.chefapi.search(index, sort_, asc,
                                             limit, offset, query or '*:*')
            numrows = len(rows)
            self.log.debug('Chef search returned %s rows' % numrows)
        except Exception:
            import traceback;
            msg = "Oops...\n" + traceback.format_exc()+"\n"
            data['message'] = _(to_unicode(msg))
            self.log.debug(data['message'])
            return 'droplet_grid.html', data, None
        
        paginator = None
        if limit > 0:
            paginator = Paginator(rows, page - 1, limit, total)
            data['paginator'] = paginator
            if paginator.has_next_page:
                add_link(req, 'next', droplet_href(page=page + 1),
                         _('Next Page'))
            if paginator.has_previous_page:
                add_link(req, 'prev', droplet_href(page=page - 1),
                         _('Previous Page'))
            
            pagedata = []
            shown_pages = paginator.get_shown_pages(21)
            for p in shown_pages:
                pagedata.append([droplet_href(page=p), None, str(p),
                                 _('Page %(num)d', num=p)])
            fields = ['href', 'class', 'string', 'title']
            paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata]
            paginator.current_page = {'href': None, 'class': 'current',
                                    'string': str(paginator.page + 1),
                                    'title': None}
            numrows = paginator.num_items
        
        # Place retrieved columns in groups, according to naming conventions
        #  * _col_ means fullrow, i.e. a group with one header
        #  * col_ means finish the current group and start a new one
        
        header_groups = [[]]
        for field in columns:
            header = {
                'col': field.name,
                'title': field.label,
                'hidden': False,
                'asc': None,
            }

            if field.name == sort:
                header['asc'] = asc

            header_group = header_groups[-1]
            header_group.append(header)
        
        # Structure the rows and cells:
        #  - group rows according to __group__ value, if defined
        #  - group cells the same way headers are grouped
        row_groups = []
        authorized_results = [] 
        prev_group_value = None
        for row_idx, item in enumerate(rows):
            col_idx = 0
            cell_groups = []
            row = {'cell_groups': cell_groups}
            for header_group in header_groups:
                cell_group = []
                for header in header_group:
                    col = header['col']
                    field = self.fields[col]
                    value = field.get(item, req)
                    cell = {'value': value, 'header': header, 'index': col_idx}
                    col_idx += 1
                    # Detect and create new group
                    if col == groupby and value != prev_group_value:
                        prev_group_value = value
                        row_groups.append( (value,[]) )
                    # Other row properties
                    row['__idx__'] = row_idx
                    if col == self.id_field:
                        row['id'] = value
                    cell_group.append(cell)
                cell_groups.append(cell_group)
            resource = Resource('cloud', '%s/%s' % (self.name,row['id']))
            if 'CLOUD_VIEW' not in req.perm(resource):
                continue
            authorized_results.append(item)
            row['resource'] = resource
            if row_groups:
                row_group = row_groups[-1][1]
            else:
                row_group = []
                row_groups = [(None, row_group)]
            row_group.append(row)
        
        data.update({'header_groups': header_groups,
                     'row_groups': row_groups,
                     'numrows': numrows,
                     'sorting_enabled': len(row_groups) == 1})
# FIXME: implement formats        
#        if format == 'rss':
#            data['email_map'] = Chrome(self.env).get_email_map()
#            data['context'] = Context.from_request(req, report_resource,
#                                                   absurls=True)
#            return 'report.rss', data, 'application/rss+xml'
#        elif format == 'csv':
#            filename = id and 'report_%s.csv' % id or 'report.csv'
#            self._send_csv(req, cols, authorized_results, mimetype='text/csv',
#                           filename=filename)
#        elif format == 'tab':
#            filename = id and 'report_%s.tsv' % id or 'report.tsv'
#            self._send_csv(req, cols, authorized_results, '\t',
#                           mimetype='text/tab-separated-values',
#                           filename=filename)
#        else:
        page = max is not None and page or None
        add_link(req, 'alternate', droplet_href(format='rss', page=None),
                 _('RSS Feed'), 'application/rss+xml', 'rss')
        add_link(req, 'alternate', droplet_href(format='csv', page=page),
                 _('Comma-delimited Text'), 'text/plain')
        add_link(req, 'alternate', droplet_href(format='tab', page=page),
                 _('Tab-delimited Text'), 'text/plain')

        self.log.debug('Rendered grid')
        return 'droplet_grid.html', data, None
    
    def render_view(self, req, id):
        self.log.debug('Rendering view..')
        req.perm.require('CLOUD_VIEW')
        item = self.chefapi.resource(self.crud_resource, id, self.name)
        
        data = {
            'title': _('%(label)s %(id)s', label=self.label, id=id),
            'buttons': [],
            'action': 'view',
            'droplet_name': self.name,
            'id': id,
            'label': self.label,
            'fields': self.fields.get_list('crud_view', filter=r"cmd_.*"),
            'cmd_fields': self.fields.get_list('crud_view', filter=r"(?!cmd_)"),
            'item': item,
            'req': req,
            'message': req.args.get('message')}
        
        self.log.debug('Rendered view')
        return 'droplet_view.html', data, None
    
    def render_edit(self, req, id):
        self.log.debug('Rendering edit/new..')
        if id:
            item = self.chefapi.resource(self.crud_resource, id, self.name)
        else:
            item = None
        
        data = {
            'droplet_name': self.name,
            'id': id,
            'label': self.label,
            'item': item,
            'req': req,
            'error': req.args.get('error')}
        
        # check if creating or editing
        if id:
            req.perm.require('CLOUD_MODIFY')
            data.update({
                'title': _('Edit  %(label)s %(id)s', label=self.label, id=id),
                'buttons': [('save',_('Save %(label)s', label=self.label))],
                'action': 'edit',
                'fields': self.fields.get_list('crud_edit')})
        else:
            req.perm.require('CLOUD_CREATE')
            data.update({
                'title': _('Create New %(label)s', label=self.label),
                'buttons': [('new',_('Create %(label)s', label=self.label))],
                'action': 'new',
                'fields': self.fields.get_list('crud_new')})
        
        self.log.debug('Rendered edit/new')
        return 'droplet_edit.html', data, None
    
    def render_delete(self, req, id):
        self.log.debug('Rendering delete..')
        req.perm.require('CLOUD_DELETE')
        data = {
            'title': _('Delete  %(label)s %(id)s', label=self.label, id=id),
            'button': _('Delete %(label)s', label=self.label),
            'droplet_name': self.name,
            'id': id,
            'label': self.label}
        self.log.debug('Rendered delete')
        return 'droplet_delete.html', data, None
    
    def render_progress(self, req, file):
        self.log.debug('Rendering progress..')
        req.perm.require('CLOUD_MODIFY')
        
        data = {
            'title': self.title,
            'label': self.label,
            'action': 'progress',
            'file': file,
            'droplet_name': self.name,
            'req': req,
            'error': req.args.get('error')}
        
        self.log.debug('Rendered progress')
        return 'droplet_progress.html', data, None
    
    def create(self, req):
        req.perm.require('CLOUD_CREATE')
        id = req.args.get('name')
        self.log.debug('Creating data bag item %s/%s..' % (self.name,id))
        fields = self.fields.get_list('crud_new', filter=r"cmd_.*")
        self.save(req, id, fields, redirect=False)
        add_notice(req, _('%(label)s %(id)s has been created.',
                          label=self.label, id=id))
        req.redirect(req.href.cloud(self.name, id))
    
    def save(self, req, id, fields=None, redirect=True):
        req.perm.require('CLOUD_MODIFY')
        bag = self.chefapi.resource('data', name=self.name)
        item = self.chefapi.databagitem(bag, id)
        fields = fields or self.fields.get_list('crud_edit', filter=r"cmd_.*")
        for field in fields:
            field.set(item, req, default='')
        item.save()
        self.log.info('Saved data bag item %s/%s..' % (bag.name,id))
        if redirect:
            add_notice(req, _('%(label)s %(id)s has been saved.',
                              label=self.label, id=id))
            req.redirect(req.href.cloud(self.name, id))
        
    def delete(self, req, id):
        req.perm.require('CLOUD_DELETE')
        self.log.debug('Deleting data bag item %s/%s..' % (self.name,id))
        bag = self.chefapi.resource('data', name=self.name)
        item = self.chefapi.databagitem(bag, id)
        item.delete()
        self.log.info('Deleted data bag item %s/%s' % (self.name,id))
        add_notice(req, _('%(label)s %(id)s has been deleted.',
                          label=self.label, id=id))
        req.redirect(req.href.cloud(self.name))
        
    def audit(self, req, id=None, redirect=True):
        req.redirect(req.href.cloud(self.name))
    
    def _spawn(self, req, exe, launch_data, attributes, progress_file=None):
        """Helper function to spawn processes with progress tracking."""
        if not progress_file:
            progress_file = Progress.get_file()
        
        # create the command
        cmd = [
           '/usr/bin/python', exe, '--daemonize',
           '--trac-base-url="%s"' % self.trac_base_url,
           '--progress-file="%s"' % progress_file,
           '--log-file="%s"' %  self.log.handlers[0].stream.name,
           '--chef-base-path="%s"' % self.chefapi.base_path,
           '--aws-key="%s"' % self.cloudapi.key,
           '--aws-secret="%s"' % self.cloudapi.secret,
           '--aws-keypair="%s"' % self.cloudapi.keypair,
           '--aws-keypair-pem="%s"' % self.chefapi.keypair_pem,
           '--aws-username="******"' % self.chefapi.user,
           '--aws-security-groups="%s"' % self.cloudapi.security_groups,
           '--rds-username="******"' % self.cloudapi.username,
           '--rds-password="******"' % self.cloudapi.password,
           '--databag="%s"' % self.name,
           "--launch-data='%s'" % json.dumps(launch_data),
           "--attributes='%s'" % json.dumps(attributes),
           '--started-by="%s"' % req.authname,
           '--notify-jabber="%s"' % self.notify_jabber,
           '--jabber-server="%s"' % self.jabber_server,
           '--jabber-port="%s"' % self.jabber_port,
           '--jabber-server="%s"' % self.jabber_server,
           '--jabber-username="******"' % self.jabber_username,
           '--jabber-password="******"' % self.jabber_password,
           '--jabber-channel="%s"' % self.jabber_channel,
        ]
        cmd += ['--chef-boot-run-list="%s"' % \
            r for r in self.chefapi.boot_run_list]
        if self.chefapi.boot_sudo:
            cmd += ['--chef-boot-sudo']
        if self.chefapi.boot_version:
            cmd += ['--chef-boot-version="%s"' % self.chefapi.boot_version]
        cmd = ' '.join(cmd)
        
        # Spawn command as daemon to launch and bootstrap instance in background
        self.log.debug('Daemonizing command: %s' % cmd)
        if subprocess.call(cmd, shell=True):
            add_warning(req, _("Error daemonizing: %(cmd)s", cmd=cmd))
            req.redirect(req.href.cloud(self.name))
        req.redirect(req.href.cloud(self.name, action='progress',
                                               file=progress_file))