File: storage_utils.py

package info (click to toggle)
python-softlayer 6.2.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 7,508 kB
  • sloc: python: 57,195; makefile: 133; xml: 97; sh: 59
file content (1028 lines) | stat: -rw-r--r-- 42,269 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
"""
    SoftLayer.storage_utils
    ~~~~~~~~~~~~~~~~~~~~~~~
    Utility functions used by File and Block Storage Managers

    :license: MIT, see LICENSE for more details.
"""
from SoftLayer import exceptions
from SoftLayer import utils

# pylint: disable=too-many-lines


ENDURANCE_TIERS = {
    0.25: 100,
    2: 200,
    4: 300,
    10: 1000,
}


def populate_host_templates(hardware_ids=None,
                            virtual_guest_ids=None,
                            ip_address_ids=None,
                            subnet_ids=None):
    """Returns a populated array with the IDs provided

    :param hardware_ids: A List of SoftLayer_Hardware ids
    :param virtual_guest_ids: A List of SoftLayer_Virtual_Guest ids
    :param ip_address_ids: A List of SoftLayer_Network_Subnet_IpAddress ids
    :param subnet_ids: A List of SoftLayer_Network_Subnet ids
    :return: array of objects formatted for allowAccessFromHostList
    """
    host_templates = []
    if hardware_ids is not None:
        for hardware_id in hardware_ids:
            host_templates.append({
                'objectType': 'SoftLayer_Hardware',
                'id': hardware_id
            })

    if virtual_guest_ids is not None:
        for virtual_guest_id in virtual_guest_ids:
            host_templates.append({
                'objectType': 'SoftLayer_Virtual_Guest',
                'id': virtual_guest_id
            })

    if ip_address_ids is not None:
        for ip_address_id in ip_address_ids:
            host_templates.append({
                'objectType': 'SoftLayer_Network_Subnet_IpAddress',
                'id': ip_address_id
            })

    if subnet_ids is not None:
        for subnet_id in subnet_ids:
            host_templates.append({
                'objectType': 'SoftLayer_Network_Subnet',
                'id': subnet_id
            })
    return host_templates


def get_package(manager, category_code):
    """Returns a product package based on type of storage.

    :param manager: The storage manager which calls this function.
    :param category_code: Category code of product package.
    :return: Returns a packaged based on type of storage.
    """

    _filter = utils.NestedDict({})
    _filter['categories']['categoryCode'] = (
        utils.query_filter(category_code))
    _filter['statusCode'] = utils.query_filter('ACTIVE')

    packages = manager.client.call(
        'Product_Package', 'getAllObjects',
        filter=_filter.to_dict(),
        mask='id,name,items[prices[categories],attributes]'
    )
    if len(packages) == 0:
        raise ValueError('No packages were found for %s' % category_code)
    if len(packages) > 1:
        raise ValueError('More than one package was found for %s'
                         % category_code)

    return packages[0]


def get_location_id(manager, location):
    """Returns location id

    :param manager: The storage manager which calls this function.
    :param location: Datacenter short name
    :return: Returns location id
    """
    loc_svc = manager.client['Location_Datacenter']
    datacenters = loc_svc.getDatacenters(mask='mask[longName,id,name]')
    for datacenter in datacenters:
        if datacenter['name'] == location:
            location = datacenter['id']
            return location
    raise ValueError('Invalid datacenter name specified.')


def find_price_by_category(package, price_category):
    """Find the price in the given package that has the specified category

    :param package: The AsAService, Enterprise, or Performance product package
    :param price_category: The price category code to search for
    :return: Returns the price for the given category, or an error if not found
    """
    for item in package['items']:
        price_id = _find_price_id(item['prices'], price_category)
        if price_id:
            return price_id

    raise ValueError("Could not find price with the category, %s" % price_category)


def find_ent_space_price(package, category, size, tier_level):
    """Find the space price for the given category, size, and tier

    :param package: The Enterprise (Endurance) product package
    :param category: The category of space (endurance, replication, snapshot)
    :param size: The size for which a price is desired
    :param tier_level: The endurance tier for which a price is desired
    :return: Returns the matching price, or an error if not found
    """
    if category == 'snapshot':
        category_code = 'storage_snapshot_space'
    elif category == 'replication':
        category_code = 'performance_storage_replication'
    else:  # category == 'endurance'
        category_code = 'performance_storage_space'

    level = ENDURANCE_TIERS.get(tier_level)

    for item in package['items']:
        if int(item['capacity']) != size:
            continue
        price_id = _find_price_id(item['prices'], category_code, 'STORAGE_TIER_LEVEL', level)
        if price_id:
            return price_id

    raise ValueError("Could not find price for %s storage space" % category)


def find_ent_endurance_tier_price(package, tier_level):
    """Find the price in the given package with the specified tier level

    :param package: The Enterprise (Endurance) product package
    :param tier_level: The endurance tier for which a price is desired
    :return: Returns the price for the given tier, or an error if not found
    """
    for item in package['items']:
        for attribute in item.get('attributes', []):
            if int(attribute['value']) == ENDURANCE_TIERS.get(tier_level):
                break
        else:
            continue

        price_id = _find_price_id(item['prices'], 'storage_tier_level')
        if price_id:
            return price_id

    raise ValueError("Could not find price for endurance tier level")


def find_endurance_tier_iops_per_gb(volume):
    """Find the tier for the given endurance volume (IOPS per GB)

    :param volume: The volume for which the tier level is desired
    :return: Returns a float value indicating the IOPS per GB for the volume
    """
    tier = volume['storageTierLevel']
    iops_per_gb = 0.25

    if tier == "LOW_INTENSITY_TIER":
        iops_per_gb = 0.25
    elif tier == "READHEAVY_TIER":
        iops_per_gb = 2
    elif tier == "WRITEHEAVY_TIER":
        iops_per_gb = 4
    elif tier == "10_IOPS_PER_GB":
        iops_per_gb = 10
    else:
        raise ValueError("Could not find tier IOPS per GB for this volume")

    return iops_per_gb


def find_perf_space_price(package, size):
    """Find the price in the given package with the specified size

    :param package: The Performance product package
    :param size: The storage space size for which a price is desired
    :return: Returns the price for the given size, or an error if not found
    """
    for item in package['items']:
        if int(item['capacity']) != size:
            continue

        price_id = _find_price_id(item['prices'], 'performance_storage_space')
        if price_id:
            return price_id

    raise ValueError("Could not find performance space price for this volume")


def find_perf_iops_price(package, size, iops):
    """Find the price in the given package with the specified size and iops

    :param package: The Performance product package
    :param size: The size of storage space for which an IOPS price is desired
    :param iops: The number of IOPS for which a price is desired
    :return: Returns the price for the size and IOPS, or an error if not found
    """
    for item in package['items']:
        if int(item['capacity']) != int(iops):
            continue

        price_id = _find_price_id(item['prices'], 'performance_storage_iops', 'STORAGE_SPACE', size)
        if price_id:
            return price_id

    raise ValueError("Could not find price for iops for the given volume")


def find_saas_endurance_space_price(package, size, tier_level):
    """Find the SaaS endurance storage space price for the size and tier

    :param package: The Storage As A Service product package
    :param size: The volume size for which a price is desired
    :param tier_level: The endurance tier for which a price is desired
    :return: Returns the price for the size and tier, or an error if not found
    """
    if tier_level != 0.25:
        tier_level = int(tier_level)
    key_name = f'STORAGE_SPACE_FOR_{tier_level}_IOPS_PER_GB'
    key_name = key_name.replace(".", "_")
    for item in package['items']:
        if key_name not in item['keyName']:
            continue

        if 'capacityMinimum' not in item or 'capacityMaximum' not in item:
            continue

        capacity_minimum = int(item['capacityMinimum'])
        capacity_maximum = int(item['capacityMaximum'])
        if size < capacity_minimum or size > capacity_maximum:
            continue

        price_id = _find_price_id(item['prices'], 'performance_storage_space')
        if price_id:
            return price_id

    raise ValueError("Could not find price for endurance storage space")


def find_saas_endurance_tier_price(package, tier_level):
    """Find the SaaS storage tier level price for the specified tier level

    :param package: The Storage As A Service product package
    :param tier_level: The endurance tier for which a price is desired
    :return: Returns the price for the given tier, or an error if not found
    """
    target_capacity = ENDURANCE_TIERS.get(tier_level)
    for item in package['items']:
        if 'itemCategory' not in item\
                or 'categoryCode' not in item['itemCategory']\
                or item['itemCategory']['categoryCode']\
                != 'storage_tier_level':
            continue

        if int(item['capacity']) != target_capacity:
            continue

        price_id = _find_price_id(item['prices'], 'storage_tier_level')
        if price_id:
            return price_id

    raise ValueError("Could not find price for endurance tier level")


def find_saas_perform_space_price(package, size):
    """Find the SaaS performance storage space price for the given size

    :param package: The Storage As A Service product package
    :param size: The volume size for which a price is desired
    :return: Returns the price for the size and tier, or an error if not found
    """
    for item in package['items']:
        if 'itemCategory' not in item\
                or 'categoryCode' not in item['itemCategory']\
                or item['itemCategory']['categoryCode']\
                != 'performance_storage_space':
            continue

        if 'capacityMinimum' not in item or 'capacityMaximum' not in item:
            continue

        capacity_minimum = int(item['capacityMinimum'])
        capacity_maximum = int(item['capacityMaximum'])
        if size < capacity_minimum or size > capacity_maximum:
            continue

        key_name = f'{capacity_minimum}_{capacity_maximum}_GBS'
        if item['keyName'] != key_name:
            continue
        price_id = _find_price_id(item['prices'], 'performance_storage_space')
        if price_id:
            return price_id

    raise ValueError("Could not find price for performance storage space")


def find_saas_perform_iops_price(package, size, iops):
    """Find the SaaS IOPS price for the specified size and iops

    :param package: The Storage As A Service product package
    :param size: The volume size for which a price is desired
    :param iops: The number of IOPS for which a price is desired
    :return: Returns the price for the size and IOPS, or an error if not found
    """
    for item in package['items']:
        if 'itemCategory' not in item\
                or 'categoryCode' not in item['itemCategory']\
                or item['itemCategory']['categoryCode']\
                != 'performance_storage_iops':
            continue

        if 'capacityMinimum' not in item or 'capacityMaximum' not in item:
            continue

        capacity_minimum = int(item['capacityMinimum'])
        capacity_maximum = int(item['capacityMaximum'])
        if iops < capacity_minimum or iops > capacity_maximum:
            continue

        price_id = _find_price_id(item['prices'], 'performance_storage_iops', 'STORAGE_SPACE', size)
        if price_id:
            return price_id

    raise ValueError("Could not find price for iops for the given volume")


def find_saas_snapshot_space_price(package, size, tier=None, iops=None):
    """Find the price in the SaaS package for the desired snapshot space size

    :param package: The product package of the endurance storage type
    :param size: The snapshot space size for which a price is desired
    :param tier: The tier of the volume for which space is being ordered
    :param iops: The IOPS of the volume for which space is being ordered
    :return: Returns the price for the given size, or an error if not found
    """
    if tier is not None:
        target_value = ENDURANCE_TIERS.get(tier)
        target_restriction_type = 'STORAGE_TIER_LEVEL'
    else:
        target_value = iops
        target_restriction_type = 'IOPS'

    for item in package['items']:
        if int(item['capacity']) != size:
            continue

        price_id = _find_price_id(item['prices'], 'storage_snapshot_space', target_restriction_type, target_value)
        if price_id:
            return price_id

    raise ValueError("Could not find price for snapshot space")


def find_saas_replication_price(package, tier=None, iops=None):
    """Find the price in the given package for the desired replicant volume

    :param package: The product package of the endurance storage type
    :param tier: The tier of the primary storage volume
    :param iops: The IOPS of the primary storage volume
    :return: Returns the replication price, or an error if not found
    """
    if tier is not None:
        target_value = ENDURANCE_TIERS.get(tier)
        target_item_keyname = 'REPLICATION_FOR_TIERBASED_PERFORMANCE'
        target_restriction_type = 'STORAGE_TIER_LEVEL'
    else:
        target_value = iops
        target_item_keyname = 'REPLICATION_FOR_IOPSBASED_PERFORMANCE'
        target_restriction_type = 'IOPS'

    for item in package['items']:
        if item['keyName'] != target_item_keyname:
            continue

        price_id = _find_price_id(
            item['prices'],
            'performance_storage_replication',
            target_restriction_type,
            target_value
        )
        if price_id:
            return price_id

    raise ValueError("Could not find price for replicant volume")


def find_snapshot_schedule_id(volume, snapshot_schedule_keyname):
    """Find the snapshot schedule ID for the given volume and keyname

    :param volume: The volume for which the snapshot ID is desired
    :param snapshot_schedule_keyname: The keyname of the snapshot schedule
    :return: Returns an int value indicating the volume's snapshot schedule ID
    """
    for schedule in volume['schedules']:
        if 'type' in schedule and 'keyname' in schedule['type']:
            if schedule['type']['keyname'] == snapshot_schedule_keyname:
                return schedule['id']

    raise ValueError("The given snapshot schedule ID was not found for the given storage volume")


def prepare_snapshot_order_object(manager, volume, capacity, tier, upgrade, iops):
    """Prepare the snapshot space order object for the placeOrder() method

    :param manager: The File or Block manager calling this function
    :param integer volume: The volume for which snapshot space is ordered
    :param integer capacity: The snapshot space size to order, in GB
    :param float tier: The tier level of the volume, in IOPS per GB (optional)
    :param boolean upgrade: Flag to indicate if this order is an upgrade
    :return: Returns the order object for the
             Product_Order service's placeOrder() method
    """
    # Ensure the storage volume has not been cancelled
    if 'billingItem' not in volume:
        raise exceptions.SoftLayerError('This volume has been cancelled; unable to order snapshot space')

    # Determine and validate the storage volume's billing item category
    billing_item_category_code = volume['billingItem']['categoryCode']
    if billing_item_category_code == 'storage_as_a_service':
        order_type_is_saas = True
    elif billing_item_category_code == 'storage_service_enterprise':
        order_type_is_saas = False
    else:
        raise exceptions.SoftLayerError(
            "Snapshot space cannot be ordered for a primary volume with a "
            "billing item category code of '%s'" % billing_item_category_code)

    # Use the volume's billing item category code to get the product package
    package = get_package(manager, billing_item_category_code)

    # Find prices based on the volume's type and billing item category
    if order_type_is_saas:  # 'storage_as_a_service' package
        volume_storage_type = volume['storageType']['keyName']
        if 'ENDURANCE' in volume_storage_type:
            if tier is None:
                tier = find_endurance_tier_iops_per_gb(volume)
            prices = [find_saas_snapshot_space_price(
                package, capacity, tier=tier)]
        elif 'PERFORMANCE' in volume_storage_type:
            if not _staas_version_is_v2_or_above(volume):
                raise exceptions.SoftLayerError(
                    "Snapshot space cannot be ordered for this performance "
                    "volume since it does not support Encryption at Rest.")

            prices = [find_saas_snapshot_space_price(
                package, capacity, iops=iops)]
        else:
            raise exceptions.SoftLayerError(
                "Storage volume does not have a valid storage type "
                "(with an appropriate keyName to indicate the "
                "volume is a PERFORMANCE or an ENDURANCE volume)")
    else:  # 'storage_service_enterprise' package
        if tier is None:
            tier = find_endurance_tier_iops_per_gb(volume)
        prices = [find_ent_space_price(package, 'snapshot', capacity, tier)]

    # Currently, these types are valid for snapshot space orders, whether
    # the base volume's order container was Enterprise or AsAService
    if upgrade:
        complex_type = 'SoftLayer_Container_Product_Order_'\
                       'Network_Storage_Enterprise_SnapshotSpace_Upgrade'
    else:
        complex_type = 'SoftLayer_Container_Product_Order_'\
                       'Network_Storage_Enterprise_SnapshotSpace'

    # Determine if hourly billing should be used
    hourly_billing_flag = utils.lookup(volume, 'billingItem', 'hourlyFlag')
    if hourly_billing_flag is None:
        hourly_billing_flag = False

    # Build and return the order object
    snapshot_space_order = {
        'complexType': complex_type,
        'packageId': package['id'],
        'prices': prices,
        'quantity': 1,
        'location': volume['billingItem']['location']['id'],
        'volumeId': volume['id'],
        'useHourlyPricing': hourly_billing_flag
    }

    return snapshot_space_order


def prepare_volume_order_object(manager, storage_type, location, size,
                                iops, tier, snapshot_size, service_offering,
                                volume_type, hourly_billing_flag=False):
    """Prepare the order object which is submitted to the placeOrder() method

    :param manager: The File or Block manager calling this function
    :param storage_type: "performance" or "endurance"
    :param location: Requested datacenter location name for the ordered volume
    :param size: Desired size of the volume, in GB
    :param iops: Number of IOPs for a "Performance" volume order
    :param tier: Tier level to use for an "Endurance" volume order
    :param snapshot_size: The size of snapshot space for the volume (optional)
    :param service_offering: Requested offering package to use for the order
    :param volume_type: The type of the volume to order ('file' or 'block')
    :param hourly_billing_flag: Billing type, monthly (False) or hourly (True)
    :return: Returns the order object for the
             Product_Order service's placeOrder() method
    """
    # Ensure the volume storage type is valid
    if storage_type != 'performance' and storage_type != 'endurance':
        raise exceptions.SoftLayerError(
            "Volume storage type must be either performance or endurance")

    # Find the ID for the requested location
    try:
        location_id = get_location_id(manager, location)
    except ValueError as ex:
        message = "Invalid datacenter name specified. Please provide the lower case short name (e.g.: dal09)"
        raise exceptions.SoftLayerError(message) from ex

    # Determine the category code to use for the order (and product package)
    order_type_is_saas, order_category_code = _get_order_type_and_category(
        service_offering,
        storage_type,
        volume_type
    )

    # Get the product package for the given category code
    package = get_package(manager, order_category_code)

    # Based on the storage type and product package, build up the complex type
    # and array of price codes to include in the order object
    base_type_name = 'SoftLayer_Container_Product_Order_Network_'
    if order_type_is_saas:
        complex_type = base_type_name + 'Storage_AsAService'
        if storage_type == 'performance':
            prices = [
                find_price_by_category(package, order_category_code),
                find_price_by_category(package, 'storage_' + volume_type),
                find_saas_perform_space_price(package, size),
                find_saas_perform_iops_price(package, size, iops)
            ]
            if snapshot_size is not None:
                prices.append(find_saas_snapshot_space_price(
                    package, snapshot_size, iops=iops))
        else:  # storage_type == 'endurance'
            prices = [
                find_price_by_category(package, order_category_code),
                find_price_by_category(package, 'storage_' + volume_type),
                find_saas_endurance_space_price(package, size, tier),
                find_saas_endurance_tier_price(package, tier)
            ]
            if snapshot_size is not None:
                prices.append(find_saas_snapshot_space_price(
                    package, snapshot_size, tier=tier))
    else:  # offering package is enterprise or performance
        if storage_type == 'performance':
            if volume_type == 'block':
                complex_type = base_type_name + 'PerformanceStorage_Iscsi'
            else:
                complex_type = base_type_name + 'PerformanceStorage_Nfs'
            prices = [
                find_price_by_category(package, order_category_code),
                find_perf_space_price(package, size),
                find_perf_iops_price(package, size, iops),
            ]
        else:  # storage_type == 'endurance'
            complex_type = base_type_name + 'Storage_Enterprise'
            prices = [
                find_price_by_category(package, order_category_code),
                find_price_by_category(package, 'storage_' + volume_type),
                find_ent_space_price(package, 'endurance', size, tier),
                find_ent_endurance_tier_price(package, tier),
            ]
            if snapshot_size is not None:
                prices.append(find_ent_space_price(
                    package, 'snapshot', snapshot_size, tier))

    # Build and return the order object
    order = {
        'complexType': complex_type,
        'packageId': package['id'],
        'prices': prices,
        'quantity': 1,
        'location': location_id,
        'useHourlyPricing': hourly_billing_flag
    }

    if order_type_is_saas:
        order['volumeSize'] = size
        if storage_type == 'performance':
            order['iops'] = iops

    return order


def _get_order_type_and_category(service_offering, storage_type, volume_type):
    if service_offering == 'storage_as_a_service':
        order_type_is_saas = True
        order_category_code = 'storage_as_a_service'
    elif service_offering == 'enterprise':
        order_type_is_saas = False
        if storage_type == 'endurance':
            order_category_code = 'storage_service_enterprise'
        else:
            raise exceptions.SoftLayerError(
                "The requested offering package, '%s', is not available for "
                "the '%s' storage type." % (service_offering, storage_type))
    elif service_offering == 'performance':
        order_type_is_saas = False
        if storage_type == 'performance':
            if volume_type == 'block':
                order_category_code = 'performance_storage_iscsi'
            else:
                order_category_code = 'performance_storage_nfs'
        else:
            raise exceptions.SoftLayerError(
                "The requested offering package, '%s', is not available for "
                "the '%s' storage type." % (service_offering, storage_type))
    else:
        raise exceptions.SoftLayerError(
            "The requested service offering package is not valid. "
            "Please check the available options and try again.")

    return order_type_is_saas, order_category_code


def prepare_replicant_order_object(manager, snapshot_schedule, location,
                                   tier, volume, volume_type, iops):
    """Prepare the order object which is submitted to the placeOrder() method

    :param manager: The File or Block manager calling this function
    :param snapshot_schedule: The primary volume's snapshot
                              schedule to use for replication
    :param location: The location for the ordered replicant volume
    :param tier: The tier (IOPS per GB) of the primary volume
    :param volume: The primary volume as a SoftLayer_Network_Storage object
    :param volume_type: The type of the primary volume ('file' or 'block')
    :return: Returns the order object for the
             Product_Order service's placeOrder() method
    """
    # Ensure the primary volume and snapshot space are not set for cancellation
    if 'billingItem' not in volume\
            or volume['billingItem'].get('cancellationDate'):
        raise exceptions.SoftLayerError(
            'This volume is set for cancellation; '
            'unable to order replicant volume')

    for child in volume['billingItem']['activeChildren']:
        if child['categoryCode'] == 'storage_snapshot_space'\
                and child.get('cancellationDate'):
            raise exceptions.SoftLayerError(
                'The snapshot space for this volume is set for '
                'cancellation; unable to order replicant volume')

    # Find the ID for the requested location
    try:
        location_id = get_location_id(manager, location)
    except ValueError as ex:
        message = "Invalid datacenter name specified. Please provide the lower case short name (e.g.: dal09)"
        raise exceptions.SoftLayerError(message) from ex

    # Get sizes and properties needed for the order
    volume_size = int(volume['capacityGb'])

    billing_item_category_code = volume['billingItem']['categoryCode']
    if billing_item_category_code == 'storage_as_a_service':
        order_type_is_saas = True
    elif billing_item_category_code == 'storage_service_enterprise':
        order_type_is_saas = False
    else:
        raise exceptions.SoftLayerError(
            "A replicant volume cannot be ordered for a primary volume with a "
            "billing item category code of '%s'" % billing_item_category_code)

    if 'snapshotCapacityGb' in volume:
        snapshot_size = int(volume['snapshotCapacityGb'])
    else:
        raise exceptions.SoftLayerError(
            "Snapshot capacity not found for the given primary volume")

    snapshot_schedule_id = find_snapshot_schedule_id(
        volume,
        'SNAPSHOT_' + snapshot_schedule
    )

    # Use the volume's billing item category code to get the product package
    package = get_package(manager, billing_item_category_code)

    # Find prices based on the primary volume's type and billing item category
    if order_type_is_saas:  # 'storage_as_a_service' package
        complex_type = 'SoftLayer_Container_Product_Order_'\
                       'Network_Storage_AsAService'
        volume_storage_type = volume['storageType']['keyName']
        if 'ENDURANCE' in volume_storage_type:
            volume_is_performance = False
            if tier is None:
                tier = find_endurance_tier_iops_per_gb(volume)
            prices = [
                find_price_by_category(package, billing_item_category_code),
                find_price_by_category(package, 'storage_' + volume_type),
                find_saas_endurance_space_price(package, volume_size, tier),
                find_saas_endurance_tier_price(package, tier),
                find_saas_snapshot_space_price(
                    package, snapshot_size, tier=tier),
                find_saas_replication_price(package, tier=tier)
            ]
        elif 'PERFORMANCE' in volume_storage_type:
            if not _staas_version_is_v2_or_above(volume):
                raise exceptions.SoftLayerError(
                    "A replica volume cannot be ordered for this performance "
                    "volume since it does not support Encryption at Rest.")
            volume_is_performance = True

            prices = [
                find_price_by_category(package, billing_item_category_code),
                find_price_by_category(package, 'storage_' + volume_type),
                find_saas_perform_space_price(package, volume_size),
                find_saas_perform_iops_price(package, volume_size, iops),
                find_saas_snapshot_space_price(
                    package, snapshot_size, iops=iops),
                find_saas_replication_price(package, iops=iops)
            ]
        else:
            raise exceptions.SoftLayerError(
                "Storage volume does not have a valid storage type "
                "(with an appropriate keyName to indicate the "
                "volume is a PERFORMANCE or an ENDURANCE volume)")
    else:  # 'storage_service_enterprise' package
        complex_type = 'SoftLayer_Container_Product_Order_'\
                       'Network_Storage_Enterprise'
        volume_is_performance = False
        if tier is None:
            tier = find_endurance_tier_iops_per_gb(volume)
        prices = [
            find_price_by_category(package, billing_item_category_code),
            find_price_by_category(package, 'storage_' + volume_type),
            find_ent_space_price(package, 'endurance', volume_size, tier),
            find_ent_endurance_tier_price(package, tier),
            find_ent_space_price(package, 'snapshot', snapshot_size, tier),
            find_ent_space_price(package, 'replication', volume_size, tier)
        ]

    # Determine if hourly billing should be used
    hourly_billing_flag = utils.lookup(volume, 'billingItem', 'hourlyFlag')
    if hourly_billing_flag is None:
        hourly_billing_flag = False

    # Build and return the order object
    replicant_order = {
        'complexType': complex_type,
        'packageId': package['id'],
        'prices': prices,
        'quantity': 1,
        'location': location_id,
        'originVolumeId': volume['id'],
        'originVolumeScheduleId': snapshot_schedule_id,
        'useHourlyPricing': hourly_billing_flag
    }

    if order_type_is_saas:
        replicant_order['volumeSize'] = volume_size
        if volume_is_performance:
            replicant_order['iops'] = iops

    return replicant_order


def prepare_duplicate_order_object(manager, origin_volume, iops, tier,
                                   duplicate_size, duplicate_snapshot_size,
                                   volume_type, hourly_billing_flag=False,
                                   dependent_duplicate=False):
    """Prepare the duplicate order to submit to SoftLayer_Product::placeOrder()

    :param manager: The File or Block manager calling this function
    :param origin_volume: The origin volume which is being duplicated
    :param iops: The IOPS for the duplicate volume (performance)
    :param tier: The tier level for the duplicate volume (endurance)
    :param duplicate_size: The requested size for the duplicate volume
    :param duplicate_snapshot_size: The size for the duplicate snapshot space
    :param volume_type: The type of the origin volume ('file' or 'block')
    :param hourly_billing_flag: Billing type, monthly (False) or hourly (True)
    :param dependent_duplicate: Duplicate type, normal (False) or dependent
        duplicate (True)
    :return: Returns the order object to be passed to the
             placeOrder() method of the Product_Order service
    """

    # Verify that the origin volume has not been cancelled
    if 'billingItem' not in origin_volume:
        raise exceptions.SoftLayerError(
            "The origin volume has been cancelled; "
            "unable to order duplicate volume")

    # Verify that the origin volume has snapshot space (needed for duplication)
    if isinstance(utils.lookup(origin_volume, 'snapshotCapacityGb'), str):
        origin_snapshot_size = int(origin_volume['snapshotCapacityGb'])
    else:
        raise exceptions.SoftLayerError(
            "Snapshot space not found for the origin volume. "
            "Origin snapshot space is needed for duplication.")

    # Obtain the datacenter location ID for the duplicate
    if isinstance(utils.lookup(origin_volume, 'billingItem',
                               'location', 'id'), int):
        location_id = origin_volume['billingItem']['location']['id']
    else:
        raise exceptions.SoftLayerError(
            "Cannot find origin volume's location")

    # Ensure the origin volume is STaaS v2 or higher
    # and supports Encryption at Rest
    if not _staas_version_is_v2_or_above(origin_volume):
        raise exceptions.SoftLayerError(
            "This volume cannot be duplicated since it "
            "does not support Encryption at Rest.")

    # If no specific snapshot space was requested for the duplicate,
    # use the origin snapshot space size
    if duplicate_snapshot_size is None:
        duplicate_snapshot_size = origin_snapshot_size

    # Use the origin volume size if no size was specified for the duplicate
    if duplicate_size is None:
        duplicate_size = origin_volume['capacityGb']

    # Get the appropriate package for the order
    # ('storage_as_a_service' is currently used for duplicate volumes)
    package = get_package(manager, 'storage_as_a_service')

    # Determine the IOPS or tier level for the duplicate volume, along with
    # the type and prices for the order
    origin_storage_type = origin_volume['storageType']['keyName']
    if 'PERFORMANCE' in origin_storage_type:
        volume_is_performance = True
        if iops is None:
            iops = int(origin_volume.get('provisionedIops', 0))
            if iops <= 0:
                raise exceptions.SoftLayerError("Cannot find origin volume's provisioned IOPS")
        # Set up the price array for the order
        prices = [
            find_price_by_category(package, 'storage_as_a_service'),
            find_price_by_category(package, 'storage_' + volume_type),
            find_saas_perform_space_price(package, duplicate_size),
            find_saas_perform_iops_price(package, duplicate_size, iops),
        ]
        # Add the price code for snapshot space as well, unless 0 GB was given
        if duplicate_snapshot_size > 0:
            prices.append(find_saas_snapshot_space_price(
                package, duplicate_snapshot_size, iops=iops))

    elif 'ENDURANCE' in origin_storage_type:
        volume_is_performance = False
        if tier is None:
            tier = find_endurance_tier_iops_per_gb(origin_volume)
        # Set up the price array for the order
        prices = [
            find_price_by_category(package, 'storage_as_a_service'),
            find_price_by_category(package, 'storage_' + volume_type),
            find_saas_endurance_space_price(package, duplicate_size, tier),
            find_saas_endurance_tier_price(package, tier),
        ]
        # Add the price code for snapshot space as well, unless 0 GB was given
        if duplicate_snapshot_size > 0:
            prices.append(find_saas_snapshot_space_price(
                package, duplicate_snapshot_size, tier=tier))

    else:
        raise exceptions.SoftLayerError(
            "Origin volume does not have a valid storage type "
            "(with an appropriate keyName to indicate the "
            "volume is a PERFORMANCE or an ENDURANCE volume)")

    duplicate_order = {
        'complexType': 'SoftLayer_Container_Product_Order_'
                       'Network_Storage_AsAService',
        'packageId': package['id'],
        'prices': prices,
        'volumeSize': duplicate_size,
        'quantity': 1,
        'location': location_id,
        'duplicateOriginVolumeId': origin_volume['id'],
        'useHourlyPricing': hourly_billing_flag
    }

    if volume_is_performance:
        duplicate_order['iops'] = iops

    if dependent_duplicate:
        duplicate_order['isDependentDuplicateFlag'] = 1

    return duplicate_order


def prepare_modify_order_object(manager, volume, new_iops, new_tier, new_size):
    """Prepare the modification order to submit to SoftLayer_Product::placeOrder()

    :param manager: The File or Block manager calling this function
    :param volume: The volume which is being modified
    :param new_iops: The new IOPS for the volume (performance)
    :param new_tier: The new tier level for the volume (endurance)
    :param new_size: The requested new size for the volume
    :return: Returns the order object to be passed to the placeOrder() method of the Product_Order service
    """

    # Verify that the origin volume has not been cancelled
    if 'billingItem' not in volume:
        raise exceptions.SoftLayerError("The volume has been cancelled; unable to modify volume.")

    # Ensure the origin volume is STaaS v2 or higher and supports Encryption at Rest
    if not _staas_version_is_v2_or_above(volume):
        raise exceptions.SoftLayerError("This volume cannot be modified since it does not support Encryption at Rest.")

    # Get the appropriate package for the order ('storage_as_a_service' is currently used for modifying volumes)
    package = get_package(manager, 'storage_as_a_service')

    # Based on volume storage type, ensure at least one volume property is being modified,
    # use current values if some are not specified, and lookup price codes for the order
    volume_storage_type = volume['storageType']['keyName']
    if 'PERFORMANCE' in volume_storage_type:
        volume_is_performance = True
        if new_size is None and new_iops is None:
            raise exceptions.SoftLayerError("A size or IOPS value must be given to modify this performance volume.")

        if new_size is None:
            new_size = volume['capacityGb']
        elif new_iops is None:
            new_iops = int(volume.get('provisionedIops', 0))
            if new_iops <= 0:
                raise exceptions.SoftLayerError("Cannot find volume's provisioned IOPS.")

        # Set up the prices array for the order
        prices = [
            find_price_by_category(package, 'storage_as_a_service'),
            find_saas_perform_space_price(package, new_size),
            find_saas_perform_iops_price(package, new_size, new_iops),
        ]

    elif 'ENDURANCE' in volume_storage_type:
        volume_is_performance = False
        if new_size is None and new_tier is None:
            raise exceptions.SoftLayerError("A size or tier value must be given to modify this endurance volume.")

        if new_size is None:
            new_size = volume['capacityGb']
        elif new_tier is None:
            new_tier = find_endurance_tier_iops_per_gb(volume)

        # Set up the prices array for the order
        prices = [
            find_price_by_category(package, 'storage_as_a_service'),
            find_saas_endurance_space_price(package, new_size, new_tier),
            find_saas_endurance_tier_price(package, new_tier),
        ]

    else:
        raise exceptions.SoftLayerError("Volume does not have a valid storage type (with an appropriate "
                                        "keyName to indicate the volume is a PERFORMANCE or an ENDURANCE volume).")

    modify_order = {
        'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
        'packageId': package['id'],
        'prices': prices,
        'volume': {'id': volume['id']},
        'volumeSize': new_size
    }

    if volume_is_performance:
        modify_order['iops'] = new_iops

    return modify_order


def block_or_file(storage_type_keyname):
    """returns either 'block' or 'file'

    :param storage_type_keyname: the Network_Storage['storageType']['keyName']
    :returns: 'block' or 'file'
    """
    return 'block' if 'BLOCK_STORAGE' in storage_type_keyname else 'file'


def _has_category(categories, category_code):
    return any(
        True
        for category
        in categories
        if category['categoryCode'] == category_code
    )


def _staas_version_is_v2_or_above(volume):
    return int(volume['staasVersion']) > 1 and volume['hasEncryptionAtRest']


def _find_price_id(prices, category, restriction_type=None, restriction_value=None):
    for price in prices:
        # Only collect prices from valid location groups.
        if price['locationGroupId']:
            continue

        if restriction_type is not None and restriction_value is not None:
            if restriction_type != price['capacityRestrictionType']\
                    or restriction_value < int(price['capacityRestrictionMinimum'])\
                    or restriction_value > int(price['capacityRestrictionMaximum']):
                continue

        if not _has_category(price['categories'], category):
            continue

        return {'id': price['id']}