~andrewjbeach/juju-ci-tools/make-local-patcher

« back to all changes in this revision

Viewing changes to assess_resources_charmstore.py

  • Committer: Curtis Hovey
  • Date: 2016-09-21 17:54:26 UTC
  • mto: This revision was merged to the branch mainline in revision 1612.
  • Revision ID: curtis@canonical.com-20160921175426-hmk3wlxrl1vpfuwr
Poll for the token, whcih might be None.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
from __future__ import print_function
 
4
 
 
5
import argparse
 
6
from collections import namedtuple
 
7
from datetime import datetime
 
8
import logging
 
9
import os
 
10
import sys
 
11
from tempfile import NamedTemporaryFile
 
12
from textwrap import dedent
 
13
from uuid import uuid1
 
14
 
 
15
import requests
 
16
 
 
17
from jujucharm import (
 
18
    CharmCommand,
 
19
    local_charm_path,
 
20
)
 
21
from utility import (
 
22
    configure_logging,
 
23
    JujuAssertionError,
 
24
    scoped_environ,
 
25
    temp_dir,
 
26
)
 
27
 
 
28
__metaclass__ = type
 
29
log = logging.getLogger("assess_resources_charmstore")
 
30
 
 
31
CHARMSTORE_API_VERSION = 'v5'
 
32
 
 
33
# Stores credential details for a target charmstore
 
34
CharmstoreDetails = namedtuple(
 
35
    'CharmstoreDetails',
 
36
    ['email', 'username', 'password', 'api_url'])
 
37
 
 
38
 
 
39
# Using a run id we can create a unique charm for each test run allowing us to
 
40
# test from fresh.
 
41
class RunId:
 
42
    _run_id = None
 
43
 
 
44
    def __call__(self):
 
45
        if self._run_id is None:
 
46
            self._run_id = str(uuid1()).replace('-', '')
 
47
        return self._run_id
 
48
 
 
49
 
 
50
get_run_id = RunId()
 
51
 
 
52
 
 
53
def get_charmstore_details(credentials_file=None):
 
54
    """Returns a CharmstoreDetails from `credentials_file` or env vars.
 
55
 
 
56
    Parses the credentials file (if supplied) and environment variables to
 
57
    retrieve the charmstore details and credentials.
 
58
 
 
59
    Note. any supplied detail via environment variables will overwrite anything
 
60
    supplied in the credentials file..
 
61
 
 
62
    """
 
63
 
 
64
    required_keys = ('api_url', 'password', 'email', 'username')
 
65
 
 
66
    details = {}
 
67
    if credentials_file is not None:
 
68
        details = parse_credentials_file(credentials_file)
 
69
 
 
70
    for key in required_keys:
 
71
        env_key = 'CS_{}'.format(key.upper())
 
72
        value = os.environ.get(env_key, details.get(key))
 
73
        # Can't have empty credential details
 
74
        if value is not None:
 
75
            details[key] = value
 
76
 
 
77
    if not set(details.keys()).issuperset(required_keys):
 
78
        raise ValueError('Unable to get all details from file.')
 
79
 
 
80
    return CharmstoreDetails(**details)
 
81
 
 
82
 
 
83
def split_line_details(string):
 
84
    safe_string = string.strip()
 
85
    return safe_string.split('=', 1)[-1].strip('"')
 
86
 
 
87
 
 
88
def parse_credentials_file(credentials_file):
 
89
    details = {}
 
90
    with open(credentials_file, 'r') as creds:
 
91
        for line in creds.readlines():
 
92
            if 'STORE_CREDENTIALS' in line:
 
93
                creds = split_line_details(line)
 
94
                email_address, password = creds.split(':', 1)
 
95
                details['email'] = email_address
 
96
                details['password'] = password
 
97
                raw_username = email_address.split('@', 1)[0]
 
98
                details['username'] = raw_username.replace('.', '-')
 
99
            elif 'STORE_URL' in line:
 
100
                details['api_url'] = split_line_details(line)
 
101
    return details
 
102
 
 
103
 
 
104
def ensure_can_push_and_list_charm_with_resources(charm_bin, cs_details):
 
105
    """Ensure that a charm can be pushed to a charm store with a resource.
 
106
 
 
107
    Checks that:
 
108
      - A charm can be pushed with a resource populated with a file
 
109
      - A charm can be updated (attach) after being pushed
 
110
      - A charms resources revision is updated after a push or attach
 
111
 
 
112
    """
 
113
    charm_command = CharmCommand(charm_bin, cs_details.api_url)
 
114
    with charm_command.logged_in_user(cs_details.email, cs_details.password):
 
115
        charm_id = 'juju-qa-resources-{id}'.format(id=get_run_id())
 
116
        # Only available for juju 2.x
 
117
        charm_path = local_charm_path('dummy-resource', '2.x')
 
118
        charm_url = 'cs:~{username}/{id}-0'.format(
 
119
            username=cs_details.username, id=charm_id)
 
120
 
 
121
        # Ensure we can publish a charm with a resource
 
122
        with NamedTemporaryFile(suffix='.txt') as temp_foo_resource:
 
123
            temp_foo = temp_foo_resource.name
 
124
            populate_file_data(temp_foo)
 
125
            push_charm_with_resource(
 
126
                charm_command,
 
127
                temp_foo,
 
128
                charm_id,
 
129
                charm_path,
 
130
                resource_name='foo')
 
131
 
 
132
            # Need to grant permissions so we can access details via the http
 
133
            # api.
 
134
            grant_everyone_access(charm_command, charm_url)
 
135
 
 
136
            expected_resource_details = {'foo': 0, 'bar': -1}
 
137
            check_resource_uploaded(
 
138
                charm_command,
 
139
                charm_url,
 
140
                'foo',
 
141
                temp_foo,
 
142
                expected_resource_details)
 
143
 
 
144
        # Ensure we can attach a resource independently of pushing a charm.
 
145
        with NamedTemporaryFile(suffix='.txt') as temp_bar_resource:
 
146
            temp_bar = temp_bar_resource.name
 
147
            populate_file_data(temp_bar)
 
148
            output = attach_resource_to_charm(
 
149
                charm_command, temp_bar, charm_url, resource_name='bar')
 
150
            log.info(output)
 
151
 
 
152
            expected_resource_details = {'foo': 0, 'bar': 0}
 
153
            check_resource_uploaded(
 
154
                charm_command,
 
155
                charm_url,
 
156
                'bar',
 
157
                temp_bar,
 
158
                expected_resource_details)
 
159
 
 
160
 
 
161
def populate_file_data(filepath):
 
162
    """Write unique data to file at `filepath`."""
 
163
    datestamp = datetime.utcnow().isoformat()
 
164
    with open(filepath, 'w') as f:
 
165
        f.write('{datestamp}:{uuid}'.format(
 
166
            datestamp=datestamp,
 
167
            uuid=get_run_id()))
 
168
 
 
169
 
 
170
def push_charm_with_resource(
 
171
        charm_command, temp_file, charm_id, charm_path, resource_name):
 
172
 
 
173
    output = charm_command.run(
 
174
        'push',
 
175
        charm_path,
 
176
        charm_id,
 
177
        '--resource', '{}={}'.format(resource_name, temp_file))
 
178
    log.info('Pushing charm "{id}": {output}'.format(
 
179
        id=charm_id, output=output))
 
180
 
 
181
 
 
182
def attach_resource_to_charm(
 
183
        charm_command, temp_file, charm_id, resource_name):
 
184
 
 
185
    return charm_command.run('attach', charm_id, '{}={}'.format(
 
186
        resource_name, temp_file))
 
187
 
 
188
 
 
189
def check_resource_uploaded(
 
190
        charm_command, charm_url, resource_name, src_file, resource_details):
 
191
    for check_name, check_revno in resource_details.items():
 
192
        check_resource_uploaded_revno(
 
193
            charm_command, charm_url, check_name, check_revno)
 
194
    check_resource_uploaded_contents(
 
195
        charm_command, charm_url, resource_name, src_file)
 
196
 
 
197
 
 
198
def check_resource_uploaded_revno(
 
199
        charm_command, charm_url, resource_name, revno):
 
200
    """Parse list-resources and ensure resource revno is equal to `revno`.
 
201
 
 
202
    :raises JujuAssertionError: If the resources revision is not equal to
 
203
      `revno`
 
204
 
 
205
    """
 
206
    revno = int(revno)
 
207
    output = charm_command.run('list-resources', charm_url)
 
208
 
 
209
    for line in output.split('\n'):
 
210
        if line.startswith(resource_name):
 
211
            rev = int(line.split(None, 1)[-1])
 
212
            if rev != revno:
 
213
                raise JujuAssertionError(
 
214
                    'Failed to upload resource and increment revision number.')
 
215
            return
 
216
    raise JujuAssertionError(
 
217
        'Failed to find named resource "{}" in output'.format(resource_name))
 
218
 
 
219
 
 
220
def grant_everyone_access(charm_command, charm_url):
 
221
    output = charm_command.run('grant', charm_url, 'everyone')
 
222
    log.info('Setting permissions on charm: {}'.format(output))
 
223
 
 
224
 
 
225
def check_resource_uploaded_contents(
 
226
        charm_command, charm_url, resource_name, src_file):
 
227
    charmname = charm_url.replace('cs:', '')
 
228
    api_url = '{apiurl}/{api_version}/{charmname}/resource/{name}'.format(
 
229
        apiurl=charm_command.api_url,
 
230
        api_version=CHARMSTORE_API_VERSION,
 
231
        charmname=charmname,
 
232
        name=resource_name,
 
233
    )
 
234
    log.info('Using api url: {}'.format(api_url))
 
235
 
 
236
    res = requests.get(api_url)
 
237
 
 
238
    if not res.ok:
 
239
        raise JujuAssertionError('Failed to retrieve details: {}'.format(
 
240
            res.content))
 
241
 
 
242
    with open(src_file, 'r') as f:
 
243
        file_contents = f.read()
 
244
    resource_contents = res.content
 
245
 
 
246
    raise_if_contents_differ(resource_contents, file_contents)
 
247
 
 
248
 
 
249
def raise_if_contents_differ(resource_contents, file_contents):
 
250
    if resource_contents != file_contents:
 
251
        raise JujuAssertionError(
 
252
            dedent("""\
 
253
            Resource contents mismatch.
 
254
            Expected:
 
255
            {}
 
256
            Got:
 
257
            {}""".format(
 
258
                file_contents,
 
259
                resource_contents)))
 
260
 
 
261
 
 
262
def assess_charmstore_resources(charm_bin, credentials_file):
 
263
    with temp_dir() as fake_home:
 
264
        temp_env = os.environ.copy()
 
265
        temp_env['HOME'] = fake_home
 
266
        with scoped_environ(temp_env):
 
267
            cs_details = get_charmstore_details(credentials_file)
 
268
            ensure_can_push_and_list_charm_with_resources(
 
269
                charm_bin, cs_details)
 
270
 
 
271
 
 
272
def parse_args(argv):
 
273
    """Parse all arguments."""
 
274
    parser = argparse.ArgumentParser(description="Assess resources charmstore")
 
275
    parser.add_argument('charm_bin', help='Full path to charn binary')
 
276
    parser.add_argument(
 
277
        'credentials_file',
 
278
        help='Path to the file containing the charm store credentials and url')
 
279
    parser.add_argument(
 
280
        '--verbose', action='store_const',
 
281
        default=logging.INFO, const=logging.DEBUG,
 
282
        help='Verbose test harness output.')
 
283
    return parser.parse_args(argv)
 
284
 
 
285
 
 
286
def check_resources():
 
287
    if os.environ.get('JUJU_REPOSITORY') is None:
 
288
        raise AssertionError('JUJU_REPOSITORY required')
 
289
 
 
290
 
 
291
def main(argv=None):
 
292
    args = parse_args(argv)
 
293
    configure_logging(args.verbose)
 
294
 
 
295
    check_resources()
 
296
 
 
297
    assess_charmstore_resources(args.charm_bin, args.credentials_file)
 
298
    return 0
 
299
 
 
300
 
 
301
if __name__ == '__main__':
 
302
    sys.exit(main())