summaryrefslogtreecommitdiffstats
path: root/conductor/conductor/solver/service.py
blob: f0bb14ba31bc84ab8e7db294371fbc6593dc9a6b (plain)
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
#
# -------------------------------------------------------------------------
#   Copyright (c) 2015-2017 AT&T Intellectual Property
#   Copyright (C) 2020 Wipro Limited.
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# -------------------------------------------------------------------------
#

import collections
import cotyledon
import json
import socket
import time
import traceback

from oslo_config import cfg
from oslo_log import log

from conductor.common import db_backend
from conductor.common.models import country_latency
from conductor.common.models import order_lock
from conductor.common.models.order_lock import OrderLock
from conductor.common.models import order_lock_history
from conductor.common.models import plan
from conductor.common.models import region_placeholders
from conductor.common.models import triage_tool
from conductor.common.music import messaging as music_messaging
from conductor.common.music.model import base
import conductor.common.prometheus_metrics as PC
from conductor.common.utils import conductor_logging_util as log_util
from conductor.i18n import _LE
from conductor.i18n import _LI
from conductor import messaging
from conductor import service
from conductor.solver.optimizer import optimizer
from conductor.solver.request import parser
from conductor.solver.utils import constraint_engine_interface as cei

# To use oslo.log in services:
#
# 0. Note that conductor.service.prepare_service() bootstraps this.
#    It's set up within conductor.cmd.SERVICENAME.
# 1. Add "from oslo_log import log"
# 2. Also add "LOG = log.getLogger(__name__)"
# 3. For i18n support, import appropriate shortcuts as well:
#    "from i18n import _, _LC, _LE, _LI, _LW  # noqa"
#    (that's for primary, critical, error, info, warning)
# 4. Use LOG.info, LOG.warning, LOG.error, LOG.critical, LOG.debug, e.g.:
#    "LOG.info(_LI("Something happened with {}").format(thingie))"
# 5. Do NOT put translation wrappers around any LOG.debug text.
# 6. Be liberal with logging, especially in the absence of unit tests!
# 7. Calls to print() are verboten within the service proper.
#    Logging can be redirected! (In a CLI-side script, print() is fine.)
#
# Usage: http://docs.openstack.org/developer/oslo.i18n/usage.html

LOG = log.getLogger(__name__)

# To use oslo.config in services:
#
# 0. Note that conductor.service.prepare_service() bootstraps this.
#    It's set up within conductor.cmd.SERVICENAME.
# 1. Add "from oslo_config import cfg"
# 2. Also add "CONF = cfg.CONF"
# 3. Set a list of locally used options (SOLVER_OPTS is fine).
#    Choose key names thoughtfully. Be technology-agnostic, avoid TLAs, etc.
# 4. Register, e.g. "CONF.register_opts(SOLVER_OPTS, group='solver')"
# 5. Add file reference to opts.py (may need to use itertools.chain())
# 6. Run tox -e genconfig to build a new config template.
# 7. If you want to load an entire config from a CLI you can do this:
#    "conf = service.prepare_service([], config_files=[CONFIG_FILE])"
# 8. You can even use oslo_config from a CLI and override values on the fly,
#    e.g., "CONF.set_override('hostnames', ['music2'], 'music_api')"
#    (leave the third arg out to use the DEFAULT group).
# 9. Loading a config from a CLI is optional. So long as all the options
#    have defaults (or you override them as needed), it should all work.
#
# Docs: http://docs.openstack.org/developer/oslo.config/

CONF = cfg.CONF

SOLVER_OPTS = [
    cfg.IntOpt('workers',
               default=1,
               min=1,
               help='Number of workers for solver service. '
                    'Default value is 1.'),
    cfg.IntOpt('solver_timeout',
               default=480,
               min=1,
               help='The timeout value for solver service. '
                    'Default value is 480 seconds.'),
    cfg.BoolOpt('concurrent',
                default=False,
                help='Set to True when solver will run in active-active '
                     'mode. When set to False, solver will restart any '
                     'orphaned solving requests at startup.'),
    cfg.IntOpt('timeout',
               default=600,
               min=1,
               help='Timeout for detecting a VM is down, and other VMs can pick the plan up. '
                    'This value should be larger than solver_timeout'
                    'Default value is 10 minutes. (integer value)'),
    cfg.IntOpt('max_solver_counter',
               default=1,
               min=1)
]

CONF.register_opts(SOLVER_OPTS, group='solver')

# Pull in service opts. We use them here.
OPTS = service.OPTS
CONF.register_opts(OPTS)


class SolverServiceLauncher(object):
    """Launcher for the solver service."""
    def __init__(self, conf):

        self.conf = conf

        # Set up Music access.
        self.music = db_backend.get_client()
        self.music.keyspace_create(keyspace=conf.keyspace)

        # Dynamically create a plan class for the specified keyspace
        self.Plan = base.create_dynamic_model(
            keyspace=conf.keyspace, baseclass=plan.Plan, classname="Plan")
        self.OrderLock = base.create_dynamic_model(
            keyspace=conf.keyspace, baseclass=order_lock.OrderLock, classname="OrderLock")
        self.OrderLockHistory = base.create_dynamic_model(
            keyspace=conf.keyspace, baseclass=order_lock_history.OrderLockHistory, classname="OrderLockHistory")
        self.RegionPlaceholders = base.create_dynamic_model(
            keyspace=conf.keyspace, baseclass=region_placeholders.RegionPlaceholders, classname="RegionPlaceholders")
        self.CountryLatency = base.create_dynamic_model(
            keyspace=conf.keyspace, baseclass=country_latency.CountryLatency, classname="CountryLatency")
        self.TriageTool = base.create_dynamic_model(
            keyspace=conf.keyspace, baseclass=triage_tool.TriageTool, classname="TriageTool")
        # self.Groups = base.create_dynamic_model(
        #    keyspace=conf.keyspace, baseclass=groups.Groups, classname="Groups")
        # self.GroupRules = base.create_dynamic_model(
        #    keyspace=conf.keyspace, baseclass=group_rules.GroupRules, classname="GroupRules")

        # Initialize Prometheus metrics Endpoint
        # Solver service uses index 1
        PC._init_metrics(1)

        if not self.Plan:
            raise
        if not self.OrderLock:
            raise
        if not self.OrderLockHistory:
            raise
        if not self.RegionPlaceholders:
            raise
        if not self.CountryLatency:
            raise
        if not self.TriageTool:
            raise

    def run(self):
        kwargs = {'plan_class': self.Plan,
                  'order_locks': self.OrderLock,
                  'order_locks_history': self.OrderLockHistory,
                  'region_placeholders': self.RegionPlaceholders,
                  'country_latency': self.CountryLatency,
                  'triage_tool': self.TriageTool
                  }
        # kwargs = {'plan_class': self.Plan}
        svcmgr = cotyledon.ServiceManager()
        svcmgr.add(SolverService,
                   workers=self.conf.solver.workers,
                   args=(self.conf,), kwargs=kwargs)
        svcmgr.run()


class SolverService(cotyledon.Service):
    """Solver service."""

    # This will appear in 'ps xaf'
    name = "Conductor Solver"

    regions = collections.OrderedDict()
    countries = list()

    def __init__(self, worker_id, conf, **kwargs):
        """Initializer"""

        LOG.debug("{}".format(self.__class__.__name__))
        super(SolverService, self).__init__(worker_id)
        self._init(conf, **kwargs)
        self.running = True

    def _init(self, conf, **kwargs):
        """Set up the necessary ingredients."""
        self.conf = conf
        self.kwargs = kwargs

        self.Plan = kwargs.get('plan_class')
        self.OrderLock = kwargs.get('order_locks')
        self.OrderLockHistory = kwargs.get('order_locks_history')
        self.RegionPlaceholders = kwargs.get('region_placeholders')
        self.CountryLatency = kwargs.get('country_latency')
        self.TriageTool = kwargs.get('triage_tool')

        # Set up the RPC service(s) we want to talk to.
        self.data_service = self.setup_rpc(conf, "data")

        # Set up the cei and optimizer
        self.cei = cei.ConstraintEngineInterface(self.data_service)
        # self.optimizer = optimizer.Optimizer(conf)

        # Set up Music access.
        self.music = db_backend.get_client()
        self.solver_owner_condition = {
            "solver_owner": socket.gethostname()
        }
        self.translated_status_condition = {
            "status": self.Plan.TRANSLATED
        }
        self.solving_status_condition = {
            "status": self.Plan.SOLVING
        }

        if not self.conf.solver.concurrent:
            self._reset_solving_status()

    def _gracefully_stop(self):
        """Gracefully stop working on things"""
        pass

    def current_time_seconds(self):
        """Current time in milliseconds."""
        return int(round(time.time()))

    def _reset_solving_status(self):
        """Reset plans being solved so they are solved again.

        Use this only when the solver service is not running concurrently.
        """

        plans = self.Plan.query.get_plan_by_col("status", self.Plan.SOLVING)
        for the_plan in plans:
            the_plan.status = self.Plan.TRANSLATED
            # Use only in active-passive mode, so don't have to be atomic
            the_plan.update()

    def _restart(self):
        """Prepare to restart the service"""
        pass

    def millisec_to_sec(self, millisec):
        """Convert milliseconds to seconds"""
        return millisec / 1000

    def setup_rpc(self, conf, topic):
        """Set up the RPC Client"""
        # TODO(jdandrea): Put this pattern inside music_messaging?
        transport = messaging.get_transport(conf=conf)
        target = music_messaging.Target(topic=topic)
        client = music_messaging.RPCClient(conf=conf,
                                           transport=transport,
                                           target=target)
        return client

    def run(self):
        """Run"""
        LOG.debug("{}".format(self.__class__.__name__))
        # TODO(snarayanan): This is really meant to be a control loop
        # As long as self.running is true, we process another request.

        while self.running:

            # Delay time (Seconds) for MUSIC requests.
            time.sleep(self.conf.delay_time)

            # plans = Plan.query().all()
            # Find the first plan with a status of TRANSLATED.
            # Change its status to SOLVING.
            # Then, read the "translated" field as "template".
            json_template = None
            p = None

            requests_to_solve = dict()
            regions_maps = dict()
            country_groups = list()

            # Instead of using the query.all() method, now creating an index for 'status'
            # field in conductor.plans table, and query plans by status columns
            translated_plans = self.Plan.query.get_plan_by_col("status", self.Plan.TRANSLATED)
            solving_plans = self.Plan.query.get_plan_by_col("status", self.Plan.SOLVING)

            # combine the plans with status = 'translated' and 'solving' together
            plans = translated_plans + solving_plans

            found_translated_template = False

            for p in plans:
                if p.status == self.Plan.TRANSLATED:
                    json_template = p.translation
                    found_translated_template = True
                    break
                elif p.status == self.Plan.SOLVING and (self.current_time_seconds()
                                                        - self.millisec_to_sec(p.updated)) > self.conf.solver.timeout:
                    p.status = self.Plan.TRANSLATED
                    p.update(condition=self.solving_status_condition)
                    break

            if not json_template:
                if found_translated_template:
                    message = _LE("Plan {} status is translated, yet "
                                  "the translation wasn't found").format(p.id)
                    LOG.error(message)
                    p.status = self.Plan.ERROR
                    p.message = message
                    p.update(condition=self.translated_status_condition)
                continue

            if found_translated_template and p and p.solver_counter >= self.conf.solver.max_solver_counter:
                message = _LE("Tried {} times. Plan {} is unable to solve").format(self.conf.solver.max_solver_counter,
                                                                                   p.id)
                LOG.error(message)
                p.status = self.Plan.ERROR
                p.message = message
                p.update(condition=self.translated_status_condition)
                continue

            log_util.setLoggerFilter(LOG, self.conf.keyspace, p.id)

            p.status = self.Plan.SOLVING
            p.solver_counter += 1
            p.solver_owner = socket.gethostname()

            _is_updated = p.update(condition=self.translated_status_condition)
            if not _is_updated:
                continue

            # other VMs have updated the status and start solving the plan
            if 'FAILURE' in _is_updated:
                continue

            LOG.info(_LI("Sovling starts, changing the template status from translated to solving, "
                         "atomic update response from MUSIC {}").format(_is_updated))

            LOG.info(_LI("Plan {} with request id {} is solving by machine {}. Tried to solve it for {} times.").
                     format(p.id, p.name, p.solver_owner, p.solver_counter))

            _is_success = "FAILURE"
            request = parser.Parser()
            request.cei = self.cei
            request.request_id = p.name
            request.plan_id = p.id
            # getting the number of solutions need to provide
            num_solution = getattr(p, 'recommend_max', '1')
            if num_solution.isdigit():
                num_solution = int(num_solution)

            # TODO(inam/larry): move this part of logic inside of parser and don't apply it to distance_between
            try:
                # getting region placeholders from database and insert/put into regions_maps dictionary
                region_placeholders = self.RegionPlaceholders.query.all()
                for region in region_placeholders:
                    regions_maps.update(region.countries)

                # getting country groups from database and insert into the country_groups list
                customer_loc = ''
                location_list = json_template["conductor_solver"]["locations"]
                for location_id, location_info in location_list.items():
                    customer_loc = location_info['country']

                countries = self.CountryLatency.query.get_plan_by_col("country_name", customer_loc)
                LOG.info("Customer Location for Latency Reduction " + customer_loc)

                if len(countries) == 0:
                    LOG.info("country is not present is country latency table, looking for * wildcard entry")
                    countries = self.CountryLatency.query.get_plan_by_col("country_name", "*")
                if len(countries) != 0:
                    LOG.info("Found '*' wild card entry in country latency table")
                else:
                    msg = "No '*' wild card entry found in country latency table. No solution will be provided"
                    LOG.info(msg)
                    p.message = msg

                for country in countries:
                    country_groups = country.groups

                LOG.info("Done getting Latency Country DB Groups ")
            except Exception as error_msg:
                LOG.error("Exception thrown while reading region_placeholders and country groups information "
                          "from database. Exception message: {}".format(error_msg))

            try:
                request.parse_template(json_template, country_groups, regions_maps)
                request.assgin_constraints_to_demands()
                requests_to_solve[p.id] = request
                opt = optimizer.Optimizer(self.conf, _requests=requests_to_solve)
                solution_list = opt.get_solution(num_solution)

            except Exception as err:
                message = _LE("Plan {} status encountered a "
                              "parsing error: {}").format(p.id, err)
                LOG.error(traceback.print_exc())
                p.status = self.Plan.ERROR
                p.message = message
                while 'FAILURE' in _is_success:
                    _is_success = p.update(condition=self.solver_owner_condition)
                    LOG.info(_LI("Encountered a parsing error, changing the template status from solving to error, "
                                 "atomic update response from MUSIC {}").format(_is_success))

                continue

            LOG.info("Preparing the recommendations ")
            # checking if the order is 'initial' or 'speed changed' one
            is_speed_change = False
            if request and request.request_type == 'speed changed':
                is_speed_change = True

            recommendations = []
            if not solution_list or len(solution_list) < 1:
                # when order takes too much time to solve
                if (int(round(time.time())) - self.millisec_to_sec(p.updated)) > self.conf.solver.solver_timeout:
                    message = _LI("Plan {} is timed out, exceed the expected "
                                  "time {} seconds").format(p.id, self.conf.solver.timeout)

                # when no solution found
                else:
                    message = _LI("Plan {} search failed, no "
                                  "recommendations found by machine {}").format(p.id, p.solver_owner)
                LOG.info(message)
                # Update the plan status
                p.status = self.Plan.NOT_FOUND
                p.message = message

                # Metrics to Prometheus
                m_svc_name = p.template.get('parameters', {}).get('service_name', 'N/A')
                PC.VNF_FAILURE.labels('ONAP', m_svc_name).inc()

                while 'FAILURE' in _is_success:
                    _is_success = p.update(condition=self.solver_owner_condition)
                    LOG.info(_LI("Plan serach failed, changing the template status from solving to not found, "
                                 "atomic update response from MUSIC {}").format(_is_success))
            else:
                # Assemble recommendation result JSON
                for solution in solution_list:
                    current_rec = dict()
                    for demand_name in solution:
                        resource = solution[demand_name]

                        if not is_speed_change:
                            is_rehome = "false"
                        else:
                            is_rehome = "false" if resource.get("existing_placement") == 'true' else "true"

                        location_id = "" if resource.get("cloud_region_version") == '2.5' \
                                      else resource.get("location_id")

                        rec = {
                            # FIXME(shankar) A&AI must not be hardcoded here.
                            # Also, account for more than one Inventory Provider.
                            "inventory_provider": "aai",
                            "service_resource_id":
                                resource.get("service_resource_id"),
                            "candidate": {
                                "candidate_id": resource.get("candidate_id"),
                                "inventory_type": resource.get("inventory_type"),
                                "cloud_owner": resource.get("cloud_owner"),
                                "location_type": resource.get("location_type"),
                                "location_id": location_id,
                                "is_rehome": is_rehome},
                            "attributes": {
                                "physical-location-id":
                                    resource.get("physical_location_id"),
                                "cloud_owner": resource.get("cloud_owner"),
                                'aic_version': resource.get("cloud_region_version")},
                        }

                        if rec["candidate"]["inventory_type"] in ["nssi", "nsi", "slice_profiles", "nst"]:
                            rec["candidate"] = resource

                        if resource.get('vim-id'):
                            rec["candidate"]['vim-id'] = resource.get('vim-id')

                        if rec["candidate"]["inventory_type"] == "service":
                            rec["attributes"]["host_id"] = resource.get("host_id")
                            rec["attributes"]["service_instance_id"] = resource.get("candidate_id")
                            rec["candidate"]["host_id"] = resource.get("host_id")

                            if resource.get('vlan_key'):
                                rec["attributes"]['vlan_key'] = resource.get('vlan_key')
                            if resource.get('port_key'):
                                rec["attributes"]['port_key'] = resource.get('port_key')

                        if rec["candidate"]["inventory_type"] == "vfmodule":
                            rec["attributes"]["host_id"] = resource.get("host_id")
                            rec["attributes"]["service_instance_id"] = resource.get("service_instance_id")
                            rec["candidate"]["host_id"] = resource.get("host_id")

                            if resource.get('vlan_key'):
                                rec["attributes"]['vlan_key'] = resource.get('vlan_key')
                            if resource.get('port_key'):
                                rec["attributes"]['port_key'] = resource.get('port_key')

                            vf_module_data = rec["attributes"]
                            vf_module_data['nf-name'] = resource.get("nf-name")
                            vf_module_data['nf-id'] = resource.get("nf-id")
                            vf_module_data['nf-type'] = resource.get("nf-type")
                            vf_module_data['vnf-type'] = resource.get("vnf-type")
                            vf_module_data['vf-module-id'] = resource.get("vf-module-id")
                            vf_module_data['vf-module-name'] = resource.get("vf-module-name")
                            vf_module_data['ipv4-oam-address'] = resource.get("ipv4-oam-address")
                            vf_module_data['ipv6-oam-address'] = resource.get("ipv6-oam-address")
                            vf_module_data['vservers'] = resource.get("vservers")

                        elif rec["candidate"]["inventory_type"] == "cloud":
                            if resource.get("all_directives") and resource.get("flavor_map"):
                                rec["attributes"]["directives"] = \
                                    self.set_flavor_in_flavor_directives(
                                        resource.get("flavor_map"), resource.get("all_directives"))

                                # Metrics to Prometheus
                                m_vim_id = resource.get("vim-id")
                                m_hpa_score = resource.get("hpa_score", 0)
                                m_svc_name = p.template['parameters'].get(
                                    'service_name', 'N/A')
                                for vnfc, flavor in resource.get("flavor_map").items():
                                    PC.VNF_COMPUTE_PROFILES.labels('ONAP',
                                                                   m_svc_name,
                                                                   demand_name,
                                                                   vnfc,
                                                                   flavor,
                                                                   m_vim_id).inc()

                                PC.VNF_SCORE.labels('ONAP', m_svc_name,
                                                    demand_name,
                                                    m_hpa_score).inc()

                            if resource.get('conflict_id'):
                                rec["candidate"]["conflict_id"] = resource.get("conflict_id")

                        if resource.get('passthrough_attributes'):
                            for key, value in resource.get('passthrough_attributes').items():
                                if key in rec["attributes"]:
                                    LOG.error('Passthrough attribute {} in demand {} already exist for candidate {}'.
                                              format(key, demand_name, rec['candidate_id']))
                                else:
                                    rec["attributes"][key] = value
                        # TODO(snarayanan): Add total value to recommendations?
                        # msg = "--- total value of decision = {}"
                        # LOG.debug(msg.format(_best_path.total_value))
                        # msg = "--- total cost of decision = {}"
                        # LOG.debug(msg.format(_best_path.total_cost))
                        current_rec[demand_name] = rec

                    recommendations.append(current_rec)

                # Update the plan with the solution
                p.solution = {
                    "recommendations": recommendations
                }

                # multiple spin-ups logic
                '''
                go through list of recommendations in the solution
                for cloud candidates, check if (cloud-region-id + e2evnfkey) is in the order_locks table
                if so, insert the row with status 'parked' in order_locks, changes plan status to 'pending' in plans
                table (or other status value)
                otherwise, insert the row with status 'locked' in order_locks, and change status to 'solved' in plans
                table - continue reservation
                '''

                # clean up the data/record in order_locks table, deleting all records that failed from MSO
                order_locks = self.OrderLock.query.all()
                for order_lock_record in order_locks:

                    plans = getattr(order_lock_record, 'plans')
                    for plan_id, plan_attributes in plans.items():
                        plan_dict = json.loads(plan_attributes)

                        if plan_dict.get('status', None) == OrderLock.FAILED:
                            order_lock_record.delete()
                            LOG.info(_LI("The order lock record {} with status {} is deleted (due to failure"
                                         " spinup from MSO) from order_locks table").
                                     format(order_lock_record, plan_dict.get('status')))
                            break

                inserted_order_records_dict = dict()
                available_dependenies_set = set()

                is_inserted_to_order_locks = True
                is_conflict_id_missing = False
                is_order_translated_before_spinup = False

                for solution in solution_list:

                    for demand_name, candidate in solution.items():
                        if candidate.get('inventory_type') == 'cloud':
                            conflict_id = candidate.get('conflict_id')
                            service_resource_id = candidate.get('service_resource_id')
                            # TODO(larry): add more logic for missing conflict_id in template
                            if not conflict_id:
                                is_conflict_id_missing = True
                                break

                            available_dependenies_set.add(conflict_id)
                            # check if conflict_id exists in order_locks table
                            order_lock_record = self.OrderLock.query.get_plan_by_col("id", conflict_id)
                            if order_lock_record:
                                is_spinup_completed = getattr(order_lock_record[0], 'is_spinup_completed')
                                spinup_completed_timestamp = getattr(order_lock_record[0],
                                                                     'spinup_completed_timestamp')
                                if is_spinup_completed and spinup_completed_timestamp > p.translation_begin_timestamp:
                                    is_order_translated_before_spinup = True
                                    break
                                elif not is_spinup_completed:
                                    inserted_order_records_dict[conflict_id] = service_resource_id

                if is_conflict_id_missing:
                    message = _LE("Missing conflict identifier field for cloud candidates in the template, "
                                  "could not insert into order_locks table")
                    LOG.debug(message)
                    p.status = self.Plan.SOLVED

                elif is_order_translated_before_spinup:
                    message = _LE("Retriggering Plan {} due to the new order arrives before the "
                                  "spinup completion of the old order ").format(p.id)
                    LOG.debug(message)
                    p.rehome_plan()

                elif len(inserted_order_records_dict) > 0:

                    new_dependenies_set = available_dependenies_set - set(inserted_order_records_dict.keys())
                    dependencies = ','.join(str(s) for s in new_dependenies_set)

                    for conflict_id, service_resource_id in inserted_order_records_dict.items():
                        plan = {
                            p.id: {
                                "status": OrderLock.UNDER_SPIN_UP,
                                "created": self.current_time_millis(),
                                "updated": self.current_time_millis(),
                                "service_resource_id": service_resource_id
                            }
                        }

                        if dependencies:
                            plan[p.id]['dependencies'] = dependencies

                        order_lock_row = self.OrderLock(id=conflict_id, plans=plan)
                        response = order_lock_row.insert()

                        # TODO(larry): add more logs for inserting order lock record (insert/update)
                        LOG.info(_LI("Inserting the order lock record to order_locks table in MUSIC, "
                                     "conditional insert operation response from MUSIC {}").format(response))
                        if response and response.status_code == 200:
                            body = response.json()
                            LOG.info("Succcessfully inserted the record in order_locks table with "
                                     "the following response message {}".format(body))
                        else:
                            is_inserted_to_order_locks = False
                else:
                    for solution in solution_list:
                        for demand_name, candidate in solution.items():
                            if candidate.get('inventory_type') == 'cloud':
                                conflict_id = candidate.get('conflict_id')
                                service_resource_id = candidate.get('service_resource_id')

                                order_lock_record = self.OrderLock.query.get_plan_by_col("id", conflict_id)
                                if order_lock_record:
                                    deleting_record = order_lock_record[0]
                                    plans = getattr(deleting_record, 'plans')
                                    is_spinup_completed = getattr(deleting_record, 'is_spinup_completed')
                                    spinup_completed_timestamp = getattr(deleting_record, 'spinup_completed_timestamp')

                                    if is_spinup_completed:
                                        # persist the record in order_locks_history table
                                        order_lock_history_record = \
                                            self.OrderLockHistory(conflict_id=conflict_id, plans=plans,
                                                                  is_spinup_completed=is_spinup_completed,
                                                                  spinup_completed_timestamp=spinup_completed_timestamp
                                                                  )
                                        LOG.debug("Inserting the history record with conflict id {}"
                                                  " to order_locks_history table".format(conflict_id))
                                        order_lock_history_record.insert()
                                        # remove the older record
                                        LOG.debug("Deleting the order lock record {} from order_locks table"
                                                  .format(deleting_record))
                                        deleting_record.delete()

                                plan = {
                                    p.id: {
                                        "status": OrderLock.UNDER_SPIN_UP,
                                        "created": self.current_time_millis(),
                                        "updated": self.current_time_millis(),
                                        "service_resource_id": service_resource_id
                                    }
                                }
                                order_lock_row = self.OrderLock(id=conflict_id, plans=plan)
                                response = order_lock_row.insert()
                                # TODO(larry): add more logs for inserting order lock record (insert/update)
                                LOG.info(_LI("Inserting the order lock record to order_locks table in MUSIC, "
                                             "conditional insert operation response from MUSIC {}").format(response))
                                if response and response.status_code == 200:
                                    body = response.json()
                                    LOG.info("Succcessfully inserted the record in order_locks table "
                                             "with the following response message {}".format(body))
                                else:
                                    is_inserted_to_order_locks = False

                if not is_inserted_to_order_locks:
                    message = _LE("Plan {} status encountered an "
                                  "error while inserting order lock message to MUSIC.").format(p.id)
                    LOG.error(message)
                    p.status = self.Plan.ERROR
                    p.message = message

                elif p.status == self.Plan.SOLVING:
                    if len(inserted_order_records_dict) > 0:
                        LOG.info(_LI("The plan with id {} is parked in order_locks table,"
                                     "waiting for MSO release calls").format(p.id))
                        p.status = self.Plan.WAITING_SPINUP
                    else:
                        LOG.info(_LI("The plan with id {} is inserted in order_locks table.").
                                 format(p.id))
                        p.status = self.Plan.SOLVED

            while 'FAILURE' in _is_success \
                  and (self.current_time_seconds() - self.millisec_to_sec(p.updated)) <= self.conf.solver.timeout:
                _is_success = p.update(condition=self.solver_owner_condition)
                LOG.info(_LI("Plan search complete, changing the template status from solving to {}, "
                             "atomic update response from MUSIC {}").format(p.status, _is_success))

            LOG.info(_LI("Plan {} search complete, {} solution(s) found by machine {}").
                     format(p.id, len(solution_list), p.solver_owner))
            LOG.debug("Plan {} detailed solution: {}".
                      format(p.id, p.solution))
            LOG.info("Plan name: {}".format(p.name))

    def terminate(self):
        """Terminate"""
        LOG.debug("{}".format(self.__class__.__name__))
        self.running = False
        self._gracefully_stop()
        super(SolverService, self).terminate()

    def reload(self):
        """Reload"""
        LOG.debug("{}".format(self.__class__.__name__))
        self._restart()

    def current_time_millis(self):
        """Current time in milliseconds."""
        return int(round(time.time() * 1000))

    def set_flavor_in_flavor_directives(self, flavor_map, directives):
        '''Insert the flavor name inside the flavor_map into flavor_directives

        :param flavor_map: flavor map get
        :param directives: All the directives get from request
        '''
        keys = list(flavor_map.keys())     # Python 3 Conversion -- dict object to list object
        for ele in directives.get("directives"):
            for item in ele.get("directives"):
                if "flavor_directives" in item.get("type"):
                    for attr in item.get("attributes"):
                        attr["attribute_value"] = flavor_map.get(attr["attribute_name"]) \
                            if attr.get("attribute_name") in keys else ""
        return directives