~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/apiserver/provisioner/provisioninginfo.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2016 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package provisioner
 
5
 
 
6
import (
 
7
        "fmt"
 
8
        "sort"
 
9
        "strings"
 
10
 
 
11
        "github.com/juju/errors"
 
12
        "github.com/juju/utils/series"
 
13
        "github.com/juju/utils/set"
 
14
        "gopkg.in/juju/names.v2"
 
15
 
 
16
        "github.com/juju/juju/apiserver/common"
 
17
        "github.com/juju/juju/apiserver/common/storagecommon"
 
18
        "github.com/juju/juju/apiserver/params"
 
19
        "github.com/juju/juju/cloudconfig/instancecfg"
 
20
        "github.com/juju/juju/environs"
 
21
        "github.com/juju/juju/environs/imagemetadata"
 
22
        "github.com/juju/juju/environs/simplestreams"
 
23
        "github.com/juju/juju/environs/tags"
 
24
        "github.com/juju/juju/state"
 
25
        "github.com/juju/juju/state/cloudimagemetadata"
 
26
        "github.com/juju/juju/state/multiwatcher"
 
27
        "github.com/juju/juju/storage"
 
28
)
 
29
 
 
30
// ProvisioningInfo returns the provisioning information for each given machine entity.
 
31
func (p *ProvisionerAPI) ProvisioningInfo(args params.Entities) (params.ProvisioningInfoResults, error) {
 
32
        result := params.ProvisioningInfoResults{
 
33
                Results: make([]params.ProvisioningInfoResult, len(args.Entities)),
 
34
        }
 
35
        canAccess, err := p.getAuthFunc()
 
36
        if err != nil {
 
37
                return result, err
 
38
        }
 
39
        for i, entity := range args.Entities {
 
40
                tag, err := names.ParseMachineTag(entity.Tag)
 
41
                if err != nil {
 
42
                        result.Results[i].Error = common.ServerError(common.ErrPerm)
 
43
                        continue
 
44
                }
 
45
                machine, err := p.getMachine(canAccess, tag)
 
46
                if err == nil {
 
47
                        result.Results[i].Result, err = p.getProvisioningInfo(machine)
 
48
                }
 
49
                result.Results[i].Error = common.ServerError(err)
 
50
        }
 
51
        return result, nil
 
52
}
 
53
 
 
54
func (p *ProvisionerAPI) getProvisioningInfo(m *state.Machine) (*params.ProvisioningInfo, error) {
 
55
        cons, err := m.Constraints()
 
56
        if err != nil {
 
57
                return nil, err
 
58
        }
 
59
 
 
60
        volumes, err := p.machineVolumeParams(m)
 
61
        if err != nil {
 
62
                return nil, errors.Trace(err)
 
63
        }
 
64
 
 
65
        var jobs []multiwatcher.MachineJob
 
66
        for _, job := range m.Jobs() {
 
67
                jobs = append(jobs, job.ToParams())
 
68
        }
 
69
 
 
70
        tags, err := p.machineTags(m, jobs)
 
71
        if err != nil {
 
72
                return nil, errors.Trace(err)
 
73
        }
 
74
 
 
75
        subnetsToZones, err := p.machineSubnetsAndZones(m)
 
76
        if err != nil {
 
77
                return nil, errors.Annotate(err, "cannot match subnets to zones")
 
78
        }
 
79
 
 
80
        endpointBindings, err := p.machineEndpointBindings(m)
 
81
        if err != nil {
 
82
                return nil, errors.Annotate(err, "cannot determine machine endpoint bindings")
 
83
        }
 
84
        imageMetadata, err := p.availableImageMetadata(m)
 
85
        if err != nil {
 
86
                return nil, errors.Annotate(err, "cannot get available image metadata")
 
87
        }
 
88
        controllerCfg, err := p.st.ControllerConfig()
 
89
        if err != nil {
 
90
                return nil, errors.Annotate(err, "cannot get controller configuration")
 
91
        }
 
92
 
 
93
        return &params.ProvisioningInfo{
 
94
                Constraints:      cons,
 
95
                Series:           m.Series(),
 
96
                Placement:        m.Placement(),
 
97
                Jobs:             jobs,
 
98
                Volumes:          volumes,
 
99
                Tags:             tags,
 
100
                SubnetsToZones:   subnetsToZones,
 
101
                EndpointBindings: endpointBindings,
 
102
                ImageMetadata:    imageMetadata,
 
103
                ControllerConfig: controllerCfg,
 
104
        }, nil
 
105
}
 
106
 
 
107
// machineVolumeParams retrieves VolumeParams for the volumes that should be
 
108
// provisioned with, and attached to, the machine. The client should ignore
 
109
// parameters that it does not know how to handle.
 
110
func (p *ProvisionerAPI) machineVolumeParams(m *state.Machine) ([]params.VolumeParams, error) {
 
111
        volumeAttachments, err := m.VolumeAttachments()
 
112
        if err != nil {
 
113
                return nil, err
 
114
        }
 
115
        if len(volumeAttachments) == 0 {
 
116
                return nil, nil
 
117
        }
 
118
        modelConfig, err := p.st.ModelConfig()
 
119
        if err != nil {
 
120
                return nil, err
 
121
        }
 
122
        controllerCfg, err := p.st.ControllerConfig()
 
123
        if err != nil {
 
124
                return nil, err
 
125
        }
 
126
        allVolumeParams := make([]params.VolumeParams, 0, len(volumeAttachments))
 
127
        for _, volumeAttachment := range volumeAttachments {
 
128
                volumeTag := volumeAttachment.Volume()
 
129
                volume, err := p.st.Volume(volumeTag)
 
130
                if err != nil {
 
131
                        return nil, errors.Annotatef(err, "getting volume %q", volumeTag.Id())
 
132
                }
 
133
                storageInstance, err := storagecommon.MaybeAssignedStorageInstance(
 
134
                        volume.StorageInstance, p.st.StorageInstance,
 
135
                )
 
136
                if err != nil {
 
137
                        return nil, errors.Annotatef(err, "getting volume %q storage instance", volumeTag.Id())
 
138
                }
 
139
                volumeParams, err := storagecommon.VolumeParams(
 
140
                        volume, storageInstance, modelConfig.UUID(), controllerCfg.ControllerUUID(),
 
141
                        modelConfig, p.storagePoolManager, p.storageProviderRegistry,
 
142
                )
 
143
                if err != nil {
 
144
                        return nil, errors.Annotatef(err, "getting volume %q parameters", volumeTag.Id())
 
145
                }
 
146
                provider, err := p.storageProviderRegistry.StorageProvider(storage.ProviderType(volumeParams.Provider))
 
147
                if err != nil {
 
148
                        return nil, errors.Annotate(err, "getting storage provider")
 
149
                }
 
150
                if provider.Dynamic() {
 
151
                        // Leave dynamic storage to the storage provisioner.
 
152
                        continue
 
153
                }
 
154
                volumeAttachmentParams, ok := volumeAttachment.Params()
 
155
                if !ok {
 
156
                        // Attachment is already provisioned; this is an insane
 
157
                        // state, so we should not proceed with the volume.
 
158
                        return nil, errors.Errorf(
 
159
                                "volume %s already attached to machine %s",
 
160
                                volumeTag.Id(), m.Id(),
 
161
                        )
 
162
                }
 
163
                // Not provisioned yet, so ask the cloud provisioner do it.
 
164
                volumeParams.Attachment = &params.VolumeAttachmentParams{
 
165
                        volumeTag.String(),
 
166
                        m.Tag().String(),
 
167
                        "", // we're creating the volume, so it has no volume ID.
 
168
                        "", // we're creating the machine, so it has no instance ID.
 
169
                        volumeParams.Provider,
 
170
                        volumeAttachmentParams.ReadOnly,
 
171
                }
 
172
                allVolumeParams = append(allVolumeParams, volumeParams)
 
173
        }
 
174
        return allVolumeParams, nil
 
175
}
 
176
 
 
177
// machineTags returns machine-specific tags to set on the instance.
 
178
func (p *ProvisionerAPI) machineTags(m *state.Machine, jobs []multiwatcher.MachineJob) (map[string]string, error) {
 
179
        // Names of all units deployed to the machine.
 
180
        //
 
181
        // TODO(axw) 2015-06-02 #1461358
 
182
        // We need a worker that periodically updates
 
183
        // instance tags with current deployment info.
 
184
        units, err := m.Units()
 
185
        if err != nil {
 
186
                return nil, errors.Trace(err)
 
187
        }
 
188
        unitNames := make([]string, 0, len(units))
 
189
        for _, unit := range units {
 
190
                if !unit.IsPrincipal() {
 
191
                        continue
 
192
                }
 
193
                unitNames = append(unitNames, unit.Name())
 
194
        }
 
195
        sort.Strings(unitNames)
 
196
 
 
197
        cfg, err := p.st.ModelConfig()
 
198
        if err != nil {
 
199
                return nil, errors.Trace(err)
 
200
        }
 
201
        controllerCfg, err := p.st.ControllerConfig()
 
202
        if err != nil {
 
203
                return nil, errors.Trace(err)
 
204
        }
 
205
        machineTags := instancecfg.InstanceTags(cfg.UUID(), controllerCfg.ControllerUUID(), cfg, jobs)
 
206
        if len(unitNames) > 0 {
 
207
                machineTags[tags.JujuUnitsDeployed] = strings.Join(unitNames, " ")
 
208
        }
 
209
        return machineTags, nil
 
210
}
 
211
 
 
212
// machineSubnetsAndZones returns a map of subnet provider-specific id
 
213
// to list of availability zone names for that subnet. The result can
 
214
// be empty if there are no spaces constraints specified for the
 
215
// machine, or there's an error fetching them.
 
216
func (p *ProvisionerAPI) machineSubnetsAndZones(m *state.Machine) (map[string][]string, error) {
 
217
        mcons, err := m.Constraints()
 
218
        if err != nil {
 
219
                return nil, errors.Annotate(err, "cannot get machine constraints")
 
220
        }
 
221
        includeSpaces := mcons.IncludeSpaces()
 
222
        if len(includeSpaces) < 1 {
 
223
                // Nothing to do.
 
224
                return nil, nil
 
225
        }
 
226
        // TODO(dimitern): For the network model MVP we only use the first
 
227
        // included space and ignore the rest.
 
228
        //
 
229
        // LKK Card: https://canonical.leankit.com/Boards/View/101652562/117352306
 
230
        // LP Bug: http://pad.lv/1498232
 
231
        spaceName := includeSpaces[0]
 
232
        if len(includeSpaces) > 1 {
 
233
                logger.Debugf(
 
234
                        "using space %q from constraints for machine %q (ignoring remaining: %v)",
 
235
                        spaceName, m.Id(), includeSpaces[1:],
 
236
                )
 
237
        }
 
238
        space, err := p.st.Space(spaceName)
 
239
        if err != nil {
 
240
                return nil, errors.Trace(err)
 
241
        }
 
242
        subnets, err := space.Subnets()
 
243
        if err != nil {
 
244
                return nil, errors.Trace(err)
 
245
        }
 
246
        if len(subnets) == 0 {
 
247
                return nil, errors.Errorf("cannot use space %q as deployment target: no subnets", spaceName)
 
248
        }
 
249
        subnetsToZones := make(map[string][]string, len(subnets))
 
250
        for _, subnet := range subnets {
 
251
                warningPrefix := fmt.Sprintf(
 
252
                        "not using subnet %q in space %q for machine %q provisioning: ",
 
253
                        subnet.CIDR(), spaceName, m.Id(),
 
254
                )
 
255
                providerId := subnet.ProviderId()
 
256
                if providerId == "" {
 
257
                        logger.Warningf(warningPrefix + "no ProviderId set")
 
258
                        continue
 
259
                }
 
260
                // TODO(dimitern): Once state.Subnet supports multiple zones,
 
261
                // use all of them below.
 
262
                //
 
263
                // LKK Card: https://canonical.leankit.com/Boards/View/101652562/119979611
 
264
                zone := subnet.AvailabilityZone()
 
265
                if zone == "" {
 
266
                        logger.Warningf(warningPrefix + "no availability zone(s) set")
 
267
                        continue
 
268
                }
 
269
                subnetsToZones[string(providerId)] = []string{zone}
 
270
        }
 
271
        return subnetsToZones, nil
 
272
}
 
273
 
 
274
func (p *ProvisionerAPI) machineEndpointBindings(m *state.Machine) (map[string]string, error) {
 
275
        units, err := m.Units()
 
276
        if err != nil {
 
277
                return nil, errors.Trace(err)
 
278
        }
 
279
 
 
280
        spacesNamesToProviderIds, err := p.allSpaceNamesToProviderIds()
 
281
        if err != nil {
 
282
                return nil, errors.Trace(err)
 
283
        }
 
284
 
 
285
        var combinedBindings map[string]string
 
286
        processedServicesSet := set.NewStrings()
 
287
        for _, unit := range units {
 
288
                if !unit.IsPrincipal() {
 
289
                        continue
 
290
                }
 
291
                service, err := unit.Application()
 
292
                if err != nil {
 
293
                        return nil, err
 
294
                }
 
295
                if processedServicesSet.Contains(service.Name()) {
 
296
                        // Already processed, skip it.
 
297
                        continue
 
298
                }
 
299
                bindings, err := service.EndpointBindings()
 
300
                if err != nil {
 
301
                        return nil, err
 
302
                }
 
303
                processedServicesSet.Add(service.Name())
 
304
 
 
305
                if len(bindings) == 0 {
 
306
                        continue
 
307
                }
 
308
                if combinedBindings == nil {
 
309
                        combinedBindings = make(map[string]string)
 
310
                }
 
311
 
 
312
                for endpoint, spaceName := range bindings {
 
313
                        if spaceName == "" {
 
314
                                // Skip unspecified bindings, as they won't affect the instance
 
315
                                // selected for provisioning.
 
316
                                continue
 
317
                        }
 
318
 
 
319
                        spaceProviderId, nameKnown := spacesNamesToProviderIds[spaceName]
 
320
                        if nameKnown {
 
321
                                combinedBindings[endpoint] = spaceProviderId
 
322
                        } else {
 
323
                                // Technically, this can't happen in practice, as we're
 
324
                                // validating the bindings during service deployment.
 
325
                                return nil, errors.Errorf("unknown space %q with no provider ID specified for endpoint %q", spaceName, endpoint)
 
326
                        }
 
327
                }
 
328
        }
 
329
        return combinedBindings, nil
 
330
}
 
331
 
 
332
func (p *ProvisionerAPI) allSpaceNamesToProviderIds() (map[string]string, error) {
 
333
        allSpaces, err := p.st.AllSpaces()
 
334
        if err != nil {
 
335
                return nil, errors.Annotate(err, "getting all spaces")
 
336
        }
 
337
 
 
338
        namesToProviderIds := make(map[string]string, len(allSpaces))
 
339
        for _, space := range allSpaces {
 
340
                name := space.Name()
 
341
 
 
342
                // For providers without native support for spaces, use the name instead
 
343
                // as provider ID.
 
344
                providerId := string(space.ProviderId())
 
345
                if len(providerId) == 0 {
 
346
                        providerId = name
 
347
                }
 
348
 
 
349
                namesToProviderIds[name] = providerId
 
350
        }
 
351
 
 
352
        return namesToProviderIds, nil
 
353
}
 
354
 
 
355
// availableImageMetadata returns all image metadata available to this machine
 
356
// or an error fetching them.
 
357
func (p *ProvisionerAPI) availableImageMetadata(m *state.Machine) ([]params.CloudImageMetadata, error) {
 
358
        imageConstraint, env, err := p.constructImageConstraint(m)
 
359
        if err != nil {
 
360
                return nil, errors.Annotate(err, "could not construct image constraint")
 
361
        }
 
362
 
 
363
        // Look for image metadata in state.
 
364
        data, err := p.findImageMetadata(imageConstraint, env)
 
365
        if err != nil {
 
366
                return nil, err
 
367
        }
 
368
        sort.Sort(metadataList(data))
 
369
        logger.Debugf("available image metadata for provisioning: %v", data)
 
370
        return data, nil
 
371
}
 
372
 
 
373
// constructImageConstraint returns model-specific criteria used to look for image metadata.
 
374
func (p *ProvisionerAPI) constructImageConstraint(m *state.Machine) (*imagemetadata.ImageConstraint, environs.Environ, error) {
 
375
        // If we can determine current region,
 
376
        // we want only metadata specific to this region.
 
377
        cloud, env, err := p.obtainEnvCloudConfig()
 
378
        if err != nil {
 
379
                return nil, nil, errors.Trace(err)
 
380
        }
 
381
 
 
382
        lookup := simplestreams.LookupParams{
 
383
                Series: []string{m.Series()},
 
384
                Stream: env.Config().ImageStream(),
 
385
        }
 
386
 
 
387
        mcons, err := m.Constraints()
 
388
        if err != nil {
 
389
                return nil, nil, errors.Annotatef(err, "cannot get machine constraints for machine %v", m.MachineTag().Id())
 
390
        }
 
391
 
 
392
        if mcons.Arch != nil {
 
393
                lookup.Arches = []string{*mcons.Arch}
 
394
        }
 
395
        if cloud != nil {
 
396
                lookup.CloudSpec = *cloud
 
397
        }
 
398
 
 
399
        return imagemetadata.NewImageConstraint(lookup), env, nil
 
400
}
 
401
 
 
402
// obtainEnvCloudConfig returns environment specific cloud information
 
403
// to be used in search for compatible images and their metadata.
 
404
func (p *ProvisionerAPI) obtainEnvCloudConfig() (*simplestreams.CloudSpec, environs.Environ, error) {
 
405
        env, err := environs.GetEnviron(p.configGetter, environs.New)
 
406
        if err != nil {
 
407
                return nil, nil, errors.Annotate(err, "could not get model")
 
408
        }
 
409
 
 
410
        if inst, ok := env.(simplestreams.HasRegion); ok {
 
411
                cloud, err := inst.Region()
 
412
                if err != nil {
 
413
                        // can't really find images if we cannot determine cloud region
 
414
                        // TODO (anastasiamac 2015-12-03) or can we?
 
415
                        return nil, nil, errors.Annotate(err, "getting provider region information (cloud spec)")
 
416
                }
 
417
                return &cloud, env, nil
 
418
        }
 
419
        return nil, env, nil
 
420
}
 
421
 
 
422
// findImageMetadata returns all image metadata or an error fetching them.
 
423
// It looks for image metadata in state.
 
424
// If none are found, we fall back on original image search in simple streams.
 
425
func (p *ProvisionerAPI) findImageMetadata(imageConstraint *imagemetadata.ImageConstraint, env environs.Environ) ([]params.CloudImageMetadata, error) {
 
426
        // Look for image metadata in state.
 
427
        stateMetadata, err := p.imageMetadataFromState(imageConstraint)
 
428
        if err != nil && !errors.IsNotFound(err) {
 
429
                // look into simple stream if for some reason can't get from controller,
 
430
                // so do not exit on error.
 
431
                logger.Infof("could not get image metadata from controller: %v", err)
 
432
        }
 
433
        logger.Debugf("got from controller %d metadata", len(stateMetadata))
 
434
        // No need to look in data sources if found in state.
 
435
        if len(stateMetadata) != 0 {
 
436
                return stateMetadata, nil
 
437
        }
 
438
 
 
439
        // If no metadata is found in state, fall back to original simple stream search.
 
440
        // Currently, an image metadata worker picks up this metadata periodically (daily),
 
441
        // and stores it in state. So potentially, this collection could be different
 
442
        // to what is in state.
 
443
        dsMetadata, err := p.imageMetadataFromDataSources(env, imageConstraint)
 
444
        if err != nil {
 
445
                if !errors.IsNotFound(err) {
 
446
                        return nil, errors.Trace(err)
 
447
                }
 
448
        }
 
449
        logger.Debugf("got from data sources %d metadata", len(dsMetadata))
 
450
 
 
451
        return dsMetadata, nil
 
452
}
 
453
 
 
454
// imageMetadataFromState returns image metadata stored in state
 
455
// that matches given criteria.
 
456
func (p *ProvisionerAPI) imageMetadataFromState(constraint *imagemetadata.ImageConstraint) ([]params.CloudImageMetadata, error) {
 
457
        filter := cloudimagemetadata.MetadataFilter{
 
458
                Series: constraint.Series,
 
459
                Arches: constraint.Arches,
 
460
                Region: constraint.Region,
 
461
                Stream: constraint.Stream,
 
462
        }
 
463
        stored, err := p.st.CloudImageMetadataStorage.FindMetadata(filter)
 
464
        if err != nil {
 
465
                return nil, errors.Trace(err)
 
466
        }
 
467
 
 
468
        toParams := func(m cloudimagemetadata.Metadata) params.CloudImageMetadata {
 
469
                return params.CloudImageMetadata{
 
470
                        ImageId:         m.ImageId,
 
471
                        Stream:          m.Stream,
 
472
                        Region:          m.Region,
 
473
                        Version:         m.Version,
 
474
                        Series:          m.Series,
 
475
                        Arch:            m.Arch,
 
476
                        VirtType:        m.VirtType,
 
477
                        RootStorageType: m.RootStorageType,
 
478
                        RootStorageSize: m.RootStorageSize,
 
479
                        Source:          m.Source,
 
480
                        Priority:        m.Priority,
 
481
                }
 
482
        }
 
483
 
 
484
        var all []params.CloudImageMetadata
 
485
        for _, ms := range stored {
 
486
                for _, m := range ms {
 
487
                        all = append(all, toParams(m))
 
488
                }
 
489
        }
 
490
        return all, nil
 
491
}
 
492
 
 
493
// imageMetadataFromDataSources finds image metadata that match specified criteria in existing data sources.
 
494
func (p *ProvisionerAPI) imageMetadataFromDataSources(env environs.Environ, constraint *imagemetadata.ImageConstraint) ([]params.CloudImageMetadata, error) {
 
495
        sources, err := environs.ImageMetadataSources(env)
 
496
        if err != nil {
 
497
                return nil, err
 
498
        }
 
499
 
 
500
        getStream := func(current string) string {
 
501
                if current == "" {
 
502
                        if constraint.Stream != "" {
 
503
                                return constraint.Stream
 
504
                        }
 
505
                        return env.Config().ImageStream()
 
506
                }
 
507
                return current
 
508
        }
 
509
 
 
510
        toModel := func(m *imagemetadata.ImageMetadata, mStream string, mSeries string, source string, priority int) cloudimagemetadata.Metadata {
 
511
 
 
512
                return cloudimagemetadata.Metadata{
 
513
                        cloudimagemetadata.MetadataAttributes{
 
514
                                Region:          m.RegionName,
 
515
                                Arch:            m.Arch,
 
516
                                VirtType:        m.VirtType,
 
517
                                RootStorageType: m.Storage,
 
518
                                Source:          source,
 
519
                                Series:          mSeries,
 
520
                                Stream:          mStream,
 
521
                        },
 
522
                        priority,
 
523
                        m.Id,
 
524
                }
 
525
        }
 
526
 
 
527
        var metadataState []cloudimagemetadata.Metadata
 
528
        for _, source := range sources {
 
529
                logger.Debugf("looking in data source %v", source.Description())
 
530
                found, info, err := imagemetadata.Fetch([]simplestreams.DataSource{source}, constraint)
 
531
                if err != nil {
 
532
                        // Do not stop looking in other data sources if there is an issue here.
 
533
                        logger.Warningf("encountered %v while getting published images metadata from %v", err, source.Description())
 
534
                        continue
 
535
                }
 
536
                for _, m := range found {
 
537
                        mSeries, err := series.VersionSeries(m.Version)
 
538
                        if err != nil {
 
539
                                logger.Warningf("could not determine series for image id %s: %v", m.Id, err)
 
540
                                continue
 
541
                        }
 
542
                        mStream := getStream(m.Stream)
 
543
                        metadataState = append(metadataState, toModel(m, mStream, mSeries, info.Source, source.Priority()))
 
544
                }
 
545
        }
 
546
        if len(metadataState) > 0 {
 
547
                if err := p.st.CloudImageMetadataStorage.SaveMetadata(metadataState); err != nil {
 
548
                        // No need to react here, just take note
 
549
                        logger.Warningf("failed to save published image metadata: %v", err)
 
550
                }
 
551
        }
 
552
 
 
553
        // Since we've fallen through to data sources search and have saved all needed images into controller,
 
554
        // let's try to get them from controller to avoid duplication of conversion logic here.
 
555
        all, err := p.imageMetadataFromState(constraint)
 
556
        if err != nil {
 
557
                return nil, errors.Annotate(err, "could not read metadata from controller after saving it there from data sources")
 
558
        }
 
559
 
 
560
        if len(all) == 0 {
 
561
                return nil, errors.NotFoundf("image metadata for series %v, arch %v", constraint.Series, constraint.Arches)
 
562
        }
 
563
 
 
564
        return all, nil
 
565
}
 
566
 
 
567
// metadataList is a convenience type enabling to sort
 
568
// a collection of CloudImageMetadata in order of priority.
 
569
type metadataList []params.CloudImageMetadata
 
570
 
 
571
// Implements sort.Interface
 
572
func (m metadataList) Len() int {
 
573
        return len(m)
 
574
}
 
575
 
 
576
// Implements sort.Interface and sorts image metadata by priority.
 
577
func (m metadataList) Less(i, j int) bool {
 
578
        return m[i].Priority < m[j].Priority
 
579
}
 
580
 
 
581
// Implements sort.Interface
 
582
func (m metadataList) Swap(i, j int) {
 
583
        m[i], m[j] = m[j], m[i]
 
584
}