Revision 3e1b20d6

/dev/null
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from django.core.management.base import NoArgsCommand, CommandError
35

  
36
from collections import namedtuple
37
from optparse import make_option
38
from sqlalchemy import func
39
from sqlalchemy.sql import select, and_, or_
40

  
41
from pithos.api.util import get_backend
42
from pithos.backends.modular import (
43
    CLUSTER_NORMAL, CLUSTER_HISTORY, CLUSTER_DELETED
44
)
45
clusters = (CLUSTER_NORMAL, CLUSTER_HISTORY, CLUSTER_DELETED)
46

  
47
Statistics = namedtuple('Statistics', ('node', 'path', 'size', 'cluster'))
48

  
49
ResetHoldingPayload = namedtuple('ResetHoldingPayload', (
50
                'entity', 'resource', 'key',
51
                'imported', 'exported', 'returned', 'released'))
52
ENTITY_KEY = '1'
53

  
54
backend = get_backend()
55
table = {}
56
table['nodes'] = backend.node.nodes
57
table['versions'] = backend.node.versions
58
table['statistics'] = backend.node.statistics
59
table['policy'] = backend.node.policy
60
conn = backend.node.conn
61

  
62

  
63
def _compute_statistics(nodes):
64
    statistics = []
65
    append = statistics.append
66
    for path, node in nodes:
67
        select_children = select(
68
            [table['nodes'].c.node]).where(table['nodes'].c.parent == node)
69
        select_descendants = select([table['nodes'].c.node]).where(
70
            or_(table['nodes'].c.parent.in_(select_children),
71
                table['nodes'].c.node.in_(select_children)))
72
        s = select([table['versions'].c.cluster,
73
                    func.sum(table['versions'].c.size)])
74
        s = s.group_by(table['versions'].c.cluster)
75
        s = s.where(table['nodes'].c.node == table['versions'].c.node)
76
        s = s.where(table['nodes'].c.node.in_(select_descendants))
77
        d2 = dict(conn.execute(s).fetchall())
78

  
79
        for cluster in clusters:
80
            try:
81
                size = d2[cluster]
82
            except KeyError:
83
                size = 0
84
            append(Statistics(
85
                node=node,
86
                path=path,
87
                size=size,
88
                cluster=cluster))
89
    return statistics
90

  
91

  
92
def _get_verified_usage(statistics):
93
    """Verify statistics and set quotaholder account usage"""
94
    reset_holding = []
95
    append = reset_holding.append
96
    for item in statistics:
97
        s = select([table['statistics'].c.size])
98
        s = s.where(table['statistics'].c.node == item.node)
99
        s = s.where(table['statistics'].c.cluster == item.cluster)
100
        db_item = conn.execute(s).fetchone()
101
        if not db_item:
102
            continue
103
        try:
104
            assert item.size == db_item.size, \
105
                    '%d[%s][%d], size: %d != %d' % (
106
                            item.node, item.path, item.cluster,
107
                            item.size, db_item.size)
108
        except AssertionError, e:
109
            print e
110
        if item.cluster == CLUSTER_NORMAL:
111
            append(ResetHoldingPayload(
112
                    entity=item.path,
113
                    resource='pithos+.diskspace',
114
                    key=ENTITY_KEY,
115
                    imported=item.size,
116
                    exported=0,
117
                    returned=0,
118
                    released=0))
119
    return reset_holding
120

  
121

  
122
class Command(NoArgsCommand):
123
    help = "Set quotaholder account usage"
124

  
125
    option_list = NoArgsCommand.option_list + (
126
        make_option('-a',
127
                    dest='accounts',
128
                    action='append',
129
                    help="Account to reset quota"),
130
    )
131

  
132
    def handle_noargs(self, **options):
133
        try:
134
            if not backend.quotaholder_enabled:
135
                raise CommandError('Quotaholder component is not enabled')
136

  
137
            if not backend.quotaholder_url:
138
                raise CommandError('Quotaholder component url is not set')
139

  
140
            if not backend.quotaholder_token:
141
                raise CommandError('Quotaholder component token is not set')
142

  
143
            # retrieve account nodes
144
            s = select([table['nodes'].c.path, table['nodes'].c.node])
145
            s = s.where(and_(table['nodes'].c.node != 0,
146
                             table['nodes'].c.parent == 0))
147
            if options['accounts']:
148
                s = s.where(table['nodes'].c.path.in_(options['accounts']))
149
            account_nodes = conn.execute(s).fetchall()
150

  
151
            if not account_nodes:
152
                raise CommandError('No accounts found.')
153

  
154
            # compute account statistics
155
            statistics = _compute_statistics(account_nodes)
156

  
157
            # verify and send usage
158
            reset_holding = _get_verified_usage(statistics)
159

  
160
            while True:
161
                result = backend.quotaholder.reset_holding(
162
                    context={},
163
                    clientkey='pithos',
164
                    reset_holding=reset_holding)
165

  
166
                if not result:
167
                    break
168

  
169
                missing_entities = [reset_holding[x].entity for x in result]
170
                self.stdout.write(
171
                        'Unknown quotaholder accounts: %s\n' %
172
                        ', '.join(missing_entities))
173
                m = 'Retrying sending quota usage for the rest...\n'
174
                self.stdout.write(m)
175
                missing_indexes = set(result)
176
                reset_holding = [x for i, x in enumerate(reset_holding)
177
                                 if i not in missing_indexes]
178
        finally:
179
            backend.close()
b/snf-pithos-app/pithos/api/management/commands/pithos-usage.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from django.core.management.base import NoArgsCommand, CommandError
35

  
36
from collections import namedtuple
37
from optparse import make_option
38
from sqlalchemy import func
39
from sqlalchemy.sql import select, and_, or_
40

  
41
from synnefo.util.number import strbigdec
42

  
43
from pithos.api.util import get_backend
44
from pithos.backends.modular import (
45
    CLUSTER_NORMAL, CLUSTER_HISTORY, CLUSTER_DELETED
46
)
47
clusters = (CLUSTER_NORMAL, CLUSTER_HISTORY, CLUSTER_DELETED)
48

  
49
Statistics = namedtuple('Statistics', ('node', 'path', 'size', 'cluster'))
50

  
51
class ResetHoldingPayload(namedtuple('ResetHoldingPayload', (
52
    'entity', 'resource', 'key', 'imported', 'exported', 'returned', 'released'
53
))):
54
    __slots__ = ()
55

  
56
    def __str__(self):
57
        return '%s: %s' % (self.entity, self.imported)
58

  
59

  
60
ENTITY_KEY = '1'
61

  
62
backend = get_backend()
63
table = {}
64
table['nodes'] = backend.node.nodes
65
table['versions'] = backend.node.versions
66
table['statistics'] = backend.node.statistics
67
table['policy'] = backend.node.policy
68
conn = backend.node.conn
69

  
70
def _retrieve_user_nodes(users=()):
71
    s = select([table['nodes'].c.path, table['nodes'].c.node])
72
    s = s.where(and_(table['nodes'].c.node != 0,
73
                     table['nodes'].c.parent == 0))
74
    if users:
75
        s = s.where(table['nodes'].c.path.in_(users))
76
    return conn.execute(s).fetchall()
77

  
78
def _compute_statistics(nodes):
79
    statistics = []
80
    append = statistics.append
81
    for path, node in nodes:
82
        select_children = select(
83
            [table['nodes'].c.node]).where(table['nodes'].c.parent == node)
84
        select_descendants = select([table['nodes'].c.node]).where(
85
            or_(table['nodes'].c.parent.in_(select_children),
86
                table['nodes'].c.node.in_(select_children)))
87
        s = select([table['versions'].c.cluster,
88
                    func.sum(table['versions'].c.size)])
89
        s = s.group_by(table['versions'].c.cluster)
90
        s = s.where(table['nodes'].c.node == table['versions'].c.node)
91
        s = s.where(table['nodes'].c.node.in_(select_descendants))
92
        d2 = dict(conn.execute(s).fetchall())
93

  
94
        for cluster in clusters:
95
            try:
96
                size = d2[cluster]
97
            except KeyError:
98
                size = 0
99
            append(Statistics(
100
                node=node,
101
                path=path,
102
                size=size,
103
                cluster=cluster))
104
    return statistics
105

  
106

  
107
def _get_verified_usage(statistics):
108
    """Verify statistics and set quotaholder user usage"""
109
    reset_holding = []
110
    append = reset_holding.append
111
    for item in statistics:
112
        s = select([table['statistics'].c.size])
113
        s = s.where(table['statistics'].c.node == item.node)
114
        s = s.where(table['statistics'].c.cluster == item.cluster)
115
        db_item = conn.execute(s).fetchone()
116
        if not db_item:
117
            continue
118
        try:
119
            assert item.size == db_item.size, \
120
                    '%d[%s][%d], size: %d != %d' % (
121
                            item.node, item.path, item.cluster,
122
                            item.size, db_item.size)
123
        except AssertionError, e:
124
            print e
125
        if item.cluster == CLUSTER_NORMAL:
126
            append(ResetHoldingPayload(
127
                    entity=item.path,
128
                    resource='pithos+.diskspace',
129
                    key=ENTITY_KEY,
130
                    imported=item.size,
131
                    exported=0,
132
                    returned=0,
133
                    released=0))
134
    return reset_holding
135

  
136

  
137
class Command(NoArgsCommand):
138
    help = "List and reset pithos usage"
139

  
140
    option_list = NoArgsCommand.option_list + (
141
        make_option('--list',
142
                    dest='list',
143
                    action="store_true",
144
                    default=True,
145
                    help="List usage for all or specified user"),
146
        make_option('--reset',
147
                    dest='reset',
148
                    action="store_true",
149
                    default=False,
150
                    help="Reset usage for all or specified users"),
151
        make_option('--user',
152
                    dest='users',
153
                    action='append',
154
                    metavar='USER_UUID',
155
                    help="Specify which users --list or --reset applies. This option can be repeated several times. If no user is specified --list or --reset will be applied globally."),
156
    )
157

  
158
    def handle_noargs(self, **options):
159
        try:
160
            if options['list']:
161
                user_nodes = _retrieve_user_nodes(options['users'])
162
                if not user_nodes:
163
                    raise CommandError('No users found.')
164
                statistics = _compute_statistics(user_nodes)
165
                reset_holding = _get_verified_usage(statistics)
166
                print '\n'.join([str(i) for i in reset_holding])
167

  
168
            if options['reset']:
169
                if not backend.quotaholder_enabled:
170
                    raise CommandError('Quotaholder component is not enabled')
171

  
172
                if not backend.quotaholder_url:
173
                    raise CommandError('Quotaholder url is not set')
174

  
175
                if not backend.quotaholder_token:
176
                    raise CommandError('Quotaholder token is not set')
177

  
178
                while True:
179
                    result = backend.quotaholder.reset_holding(
180
                        context={},
181
                        clientkey='pithos',
182
                        reset_holding=reset_holding)
183

  
184
                    if not result:
185
                        break
186

  
187
                    missing_entities = [reset_holding[x].entity for x in result]
188
                    self.stdout.write(
189
                            'Unknown quotaholder users: %s\n' %
190
                            ', '.join(missing_entities))
191
                    m = 'Retrying sending quota usage for the rest...\n'
192
                    self.stdout.write(m)
193
                    missing_indexes = set(result)
194
                    reset_holding = [x for i, x in enumerate(reset_holding)
195
                                     if i not in missing_indexes]
196
        finally:
197
            backend.close()

Also available in: Unified diff