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

« back to all changes in this revision

Viewing changes to assess_constraints.py

  • Committer: Curtis Hovey
  • Date: 2016-04-10 15:23:18 UTC
  • Revision ID: curtis@canonical.com-20160410152318-ntfymsv9eiw63vt2
Addedd daily streams to get_amy.py get_ami.py tests/test_get_ami.py

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
"""This module tests the deployment with constraints."""
3
 
 
4
 
from __future__ import print_function
5
 
import argparse
6
 
import logging
7
 
import os
8
 
import sys
9
 
import re
10
 
 
11
 
import yaml
12
 
 
13
 
from deploy_stack import (
14
 
    BootstrapManager,
15
 
    )
16
 
from jujucharm import (
17
 
    Charm,
18
 
    local_charm_path,
19
 
    )
20
 
from utility import (
21
 
    add_basic_testing_arguments,
22
 
    configure_logging,
23
 
    JujuAssertionError,
24
 
    temp_dir,
25
 
    )
26
 
 
27
 
 
28
 
__metaclass__ = type
29
 
 
30
 
 
31
 
log = logging.getLogger("assess_constraints")
32
 
 
33
 
VIRT_TYPES = ['lxd']
34
 
 
35
 
INSTANCE_TYPES = {
36
 
    'azure': [],
37
 
    'ec2': ['t2.micro'],
38
 
    'gce': [],
39
 
    'joyent': [],
40
 
    'openstack': [],
41
 
    }
42
 
 
43
 
 
44
 
# This assumes instances are unique accross providers.
45
 
def get_instance_spec(instance_type):
46
 
    """Get the specifications of a given instance type."""
47
 
    return {
48
 
        't2.micro': {'mem': '1G', 'cpu-power': '10', 'cores': '1'},
49
 
        }[instance_type]
50
 
 
51
 
 
52
 
def mem_to_int(size):
53
 
    """Convert an argument size into a number of megabytes."""
54
 
    if not re.match(re.compile('^[0123456789]+[MGTP]?$'), size):
55
 
        raise JujuAssertionError('Not a size format:', size)
56
 
    if size[-1] in 'MGTP':
57
 
        val = int(size[0:-1])
58
 
        unit = size[-1]
59
 
        return val * (1024 ** 'MGTP'.find(unit))
60
 
    else:
61
 
        return int(size)
62
 
 
63
 
 
64
 
class Constraints:
65
 
    """Class that represents a set of contraints."""
66
 
 
67
 
    @staticmethod
68
 
    def _list_to_str(constraints_list):
69
 
        parts = ['{}={}'.format(name, value) for (name, value) in
70
 
                 constraints_list if value is not None]
71
 
        return ' '.join(parts)
72
 
 
73
 
    def __init__(self, mem=None, cores=None, virt_type=None,
74
 
                 instance_type=None, root_disk=None, cpu_power=None,
75
 
                 arch=None):
76
 
        """Create a new constraints instance from individual constraints."""
77
 
        self.mem = mem
78
 
        self.cores = cores
79
 
        self.virt_type = virt_type
80
 
        self.instance_type = instance_type
81
 
        self.root_disk = root_disk
82
 
        self.cpu_power = cpu_power
83
 
        self.arch = arch
84
 
 
85
 
    def __str__(self):
86
 
        """Convert the instance constraint values into an argument string."""
87
 
        return Constraints._list_to_str(
88
 
            [('mem', self.mem), ('cores', self.cores),
89
 
             ('virt-type', self.virt_type),
90
 
             ('instance-type', self.instance_type),
91
 
             ('root-disk', self.root_disk), ('cpu-power', self.cpu_power),
92
 
             ('arch', self.arch),
93
 
             ])
94
 
 
95
 
    def __eq__(self, other):
96
 
        return (self.mem == other.mem and self.cores == other.cores and
97
 
                self.virt_type == other.virt_type and
98
 
                self.instance_type == other.instance_type and
99
 
                self.root_disk == other.root_disk and
100
 
                self.cpu_power == other.cpu_power and
101
 
                self.arch == other.arch
102
 
                )
103
 
 
104
 
    @staticmethod
105
 
    def _meets_string(constraint, actual):
106
 
        if constraint is None:
107
 
            return True
108
 
        return constraint == actual
109
 
 
110
 
    @staticmethod
111
 
    def _meets_min_int(constraint, actual):
112
 
        if constraint is None:
113
 
            return True
114
 
        return int(constraint) <= int(actual)
115
 
 
116
 
    @staticmethod
117
 
    def _meets_min_mem(constraint, actual):
118
 
        if constraint is None:
119
 
            return True
120
 
        return mem_to_int(constraint) <= mem_to_int(actual)
121
 
 
122
 
    def meets_root_disk(self, actual_root_disk):
123
 
        """Check to see if a given value meets the root_disk constraint."""
124
 
        return self._meets_min_mem(self.root_disk, actual_root_disk)
125
 
 
126
 
    def meets_cores(self, actual_cores):
127
 
        """Check to see if a given value meets the cores constraint."""
128
 
        return self._meets_min_int(self.cores, actual_cores)
129
 
 
130
 
    def meets_cpu_power(self, actual_cpu_power):
131
 
        """Check to see if a given value meets the cpu_power constraint."""
132
 
        return self._meets_min_int(self.cpu_power, actual_cpu_power)
133
 
 
134
 
    def meets_arch(self, actual_arch):
135
 
        """Check to see if a given value meets the arch constraint."""
136
 
        return self._meets_string(self.arch, actual_arch)
137
 
 
138
 
    def meets_instance_type(self, actual_data):
139
 
        """Check to see if a given value meets the instance_type constraint.
140
 
 
141
 
        Currently there is no direct way to check for it, so we 'fingerprint'
142
 
        each instance_type in a dictionary."""
143
 
        instance_data = get_instance_spec(self.instance_type)
144
 
        for (key, value) in instance_data.iteritems():
145
 
            # Temperary fix until cpu-cores -> cores switch is finished.
146
 
            if key is 'cores' and 'cpu-cores' in actual_data:
147
 
                key = 'cpu-cores'
148
 
            if key not in actual_data:
149
 
                raise JujuAssertionError('Missing data:', key)
150
 
            elif key in ['mem', 'root-disk']:
151
 
                if mem_to_int(value) != mem_to_int(actual_data[key]):
152
 
                    return False
153
 
            elif value != actual_data[key]:
154
 
                return False
155
 
        else:
156
 
            return True
157
 
 
158
 
 
159
 
def deploy_constraint(client, constraints, charm, series, charm_repo):
160
 
    """Test deploying charm with constraints."""
161
 
    client.deploy(charm, series=series, repository=charm_repo,
162
 
                  constraints=str(constraints))
163
 
    client.wait_for_workloads()
164
 
 
165
 
 
166
 
def deploy_charm_constraint(client, constraints, charm_name, charm_series,
167
 
                            charm_dir):
168
 
    """Create a charm with constraints and test deploying it."""
169
 
    constraints_charm = Charm(charm_name,
170
 
                              'Test charm for constraints',
171
 
                              series=[charm_series])
172
 
    charm_root = constraints_charm.to_repo_dir(charm_dir)
173
 
    platform = 'ubuntu'
174
 
    charm = local_charm_path(charm=charm_name,
175
 
                             juju_ver=client.version,
176
 
                             series=charm_series,
177
 
                             repository=os.path.dirname(charm_root),
178
 
                             platform=platform)
179
 
    deploy_constraint(client, constraints, charm,
180
 
                      charm_series, charm_dir)
181
 
 
182
 
 
183
 
def juju_show_machine_hardware(client, machine):
184
 
    """Uses juju show-machine and returns information about the hardware."""
185
 
    raw = client.get_juju_output('show-machine', machine, '--format', 'yaml')
186
 
    raw_yaml = yaml.load(raw)
187
 
    try:
188
 
        hardware = raw_yaml['machines'][machine]['hardware']
189
 
    except KeyError as error:
190
 
        raise KeyError(error.args, raw_yaml)
191
 
    data = {}
192
 
    for kvp in hardware.split(' '):
193
 
        (key, value) = kvp.split('=')
194
 
        data[key] = value
195
 
    return data
196
 
 
197
 
 
198
 
def application_machines(client, application):
199
 
    """Get all the machines used to host the given application."""
200
 
    raw = client.get_juju_output('status', '--format', 'yaml')
201
 
    raw_yaml = yaml.load(raw)
202
 
    try:
203
 
        app_data = raw_yaml['applications'][application]
204
 
        machines = []
205
 
        for (unit, unit_data) in app_data['units'].items():
206
 
            machines.append(unit_data['machine'])
207
 
        return machines
208
 
    except KeyError as error:
209
 
        raise KeyError(error.args, raw_yaml)
210
 
 
211
 
 
212
 
def prepare_constraint_test(client, constraints, charm_name,
213
 
                            charm_series='xenial'):
214
 
    """Deploy a charm with constraints and data to see if it meets them."""
215
 
    with temp_dir() as charm_dir:
216
 
        deploy_charm_constraint(client, constraints, charm_name,
217
 
                                charm_series, charm_dir)
218
 
        client.wait_for_started()
219
 
        machines = application_machines(client, charm_name)
220
 
        return juju_show_machine_hardware(client, machines[0])
221
 
 
222
 
 
223
 
def assess_virt_type(client, virt_type):
224
 
    """Assess the virt-type option for constraints"""
225
 
    if virt_type not in VIRT_TYPES:
226
 
        raise JujuAssertionError(virt_type)
227
 
    constraints = Constraints(virt_type=virt_type)
228
 
    charm_name = 'virt-type-{}'.format(virt_type)
229
 
    charm_series = 'xenial'
230
 
    with temp_dir() as charm_dir:
231
 
        deploy_charm_constraint(client, constraints, charm_name,
232
 
                                charm_series, charm_dir)
233
 
 
234
 
 
235
 
def assess_virt_type_constraints(client, test_kvm=False):
236
 
    """Assess deployment with virt-type constraints."""
237
 
    if test_kvm:
238
 
        VIRT_TYPES.append("kvm")
239
 
    for virt_type in VIRT_TYPES:
240
 
        assess_virt_type(client, virt_type)
241
 
    try:
242
 
        assess_virt_type(client, 'aws')
243
 
    except JujuAssertionError:
244
 
        log.info("Correctly rejected virt-type aws")
245
 
    else:
246
 
        raise JujuAssertionError("FAIL: Client deployed with virt-type aws")
247
 
    if test_kvm:
248
 
        VIRT_TYPES.remove("kvm")
249
 
 
250
 
 
251
 
def get_failure_exception(client, constraints):
252
 
    """Create a JujuAssertionError with a detailed error message."""
253
 
    message = 'Test Failed: on {} with constraints "{}"'.format(
254
 
        client.env.config.get('type'), str(constraints))
255
 
    return JujuAssertionError(message)
256
 
 
257
 
 
258
 
def assess_instance_type(client, provider, instance_type):
259
 
    """Assess the instance-type option for constraints"""
260
 
    if instance_type not in INSTANCE_TYPES[provider]:
261
 
        raise JujuAssertionError(instance_type)
262
 
    constraints = Constraints(instance_type=instance_type)
263
 
    charm_name = 'instance-type-{}'.format(instance_type.replace('.', '-'))
264
 
    data = prepare_constraint_test(client, constraints, charm_name)
265
 
    if not constraints.meets_instance_type(data):
266
 
        raise get_failure_exception(client, constraints)
267
 
 
268
 
 
269
 
def assess_instance_type_constraints(client, provider=None):
270
 
    """Assess deployment with instance-type constraints."""
271
 
    if provider is None:
272
 
        provider = client.env.config.get('type')
273
 
    if provider not in INSTANCE_TYPES:
274
 
        return
275
 
    for instance_type in INSTANCE_TYPES[provider]:
276
 
        assess_instance_type(client, provider, instance_type)
277
 
 
278
 
 
279
 
def assess_root_disk_constraints(client, values):
280
 
    """Assess deployment with root-disk constraints."""
281
 
    for root_disk in values:
282
 
        constraints = Constraints(root_disk=root_disk)
283
 
        charm_name = 'root-disk-{}'.format(root_disk.lower())
284
 
        data = prepare_constraint_test(client, constraints, charm_name)
285
 
        if not constraints.meets_root_disk(data['root-disk']):
286
 
            raise get_failure_exception(client, constraints)
287
 
 
288
 
 
289
 
def assess_cores_constraints(client, values):
290
 
    """Assess deployment with cores constraints."""
291
 
    for cores in values:
292
 
        constraints = Constraints(cores=cores)
293
 
        charm_name = 'cores-{}c'.format(cores)
294
 
        data = prepare_constraint_test(client, constraints, charm_name)
295
 
        if not constraints.meets_cores(data['cores']):
296
 
            raise get_failure_exception(client, constraints)
297
 
 
298
 
 
299
 
def assess_cpu_power_constraints(client, values):
300
 
    """Assess deployment with cpu_power constraints."""
301
 
    for cpu_power in values:
302
 
        constraints = Constraints(cpu_power=cpu_power)
303
 
        charm_name = 'cpu-power-{}cp'.format(cpu_power)
304
 
        data = prepare_constraint_test(client, constraints, charm_name)
305
 
        if not constraints.meets_cpu_power(data['cpu-power']):
306
 
            raise get_failure_exception(client, constraints)
307
 
 
308
 
 
309
 
def assess_constraints(client, test_kvm=False):
310
 
    """Assess deployment with constraints."""
311
 
    provider = client.env.config.get('type')
312
 
    if 'lxd' == provider:
313
 
        assess_virt_type_constraints(client, test_kvm)
314
 
    elif 'ec2' == provider:
315
 
        assess_instance_type_constraints(client, provider)
316
 
        assess_root_disk_constraints(client, ['16G'])
317
 
        assess_cores_constraints(client, ['2'])
318
 
        assess_cpu_power_constraints(client, ['30'])
319
 
 
320
 
 
321
 
def parse_args(argv):
322
 
    """Parse all arguments."""
323
 
    parser = argparse.ArgumentParser(description="Test constraints")
324
 
    add_basic_testing_arguments(parser)
325
 
    return parser.parse_args(argv)
326
 
 
327
 
 
328
 
def main(argv=None):
329
 
    args = parse_args(argv)
330
 
    configure_logging(args.verbose)
331
 
    bs_manager = BootstrapManager.from_args(args)
332
 
    test_kvm = '--with-virttype-kvm' in args
333
 
    with bs_manager.booted_context(args.upload_tools):
334
 
        assess_constraints(bs_manager.client, test_kvm)
335
 
    return 0
336
 
 
337
 
 
338
 
if __name__ == '__main__':
339
 
    sys.exit(main())