OpenStack Orchestration (Heat)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

831 lines
33 KiB

  1. #
  2. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  3. # not use this file except in compliance with the License. You may obtain
  4. # a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software
  9. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  10. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  11. # License for the specific language governing permissions and limitations
  12. # under the License.
  13. import collections
  14. import copy
  15. import functools
  16. import itertools
  17. from oslo_log import log as logging
  18. from heat.common import exception
  19. from heat.common import grouputils
  20. from heat.common.i18n import _
  21. from heat.common import timeutils
  22. from heat.engine import attributes
  23. from heat.engine import constraints
  24. from heat.engine import function
  25. from heat.engine import output
  26. from heat.engine import properties
  27. from heat.engine.resources import stack_resource
  28. from heat.engine import rsrc_defn
  29. from heat.engine import scheduler
  30. from heat.engine import support
  31. from heat.scaling import rolling_update
  32. from heat.scaling import template as scl_template
  33. LOG = logging.getLogger(__name__)
  34. class ResourceGroup(stack_resource.StackResource):
  35. """Creates one or more identically configured nested resources.
  36. In addition to the `refs` attribute, this resource implements synthetic
  37. attributes that mirror those of the resources in the group. When
  38. getting an attribute from this resource, however, a list of attribute
  39. values for each resource in the group is returned. To get attribute values
  40. for a single resource in the group, synthetic attributes of the form
  41. `resource.{resource index}.{attribute name}` can be used. The resource ID
  42. of a particular resource in the group can be obtained via the synthetic
  43. attribute `resource.{resource index}`. Note, that if you get attribute
  44. without `{resource index}`, e.g. `[resource, {attribute_name}]`, you'll get
  45. a list of this attribute's value for all resources in group.
  46. While each resource in the group will be identically configured, this
  47. resource does allow for some index-based customization of the properties
  48. of the resources in the group. For example::
  49. resources:
  50. my_indexed_group:
  51. type: OS::Heat::ResourceGroup
  52. properties:
  53. count: 3
  54. resource_def:
  55. type: OS::Nova::Server
  56. properties:
  57. # create a unique name for each server
  58. # using its index in the group
  59. name: my_server_%index%
  60. image: CentOS 6.5
  61. flavor: 4GB Performance
  62. would result in a group of three servers having the same image and flavor,
  63. but names of `my_server_0`, `my_server_1`, and `my_server_2`. The variable
  64. used for substitution can be customized by using the `index_var` property.
  65. """
  66. support_status = support.SupportStatus(version='2014.1')
  67. PROPERTIES = (
  68. COUNT, INDEX_VAR, RESOURCE_DEF, REMOVAL_POLICIES,
  69. REMOVAL_POLICIES_MODE,
  70. ) = (
  71. 'count', 'index_var', 'resource_def', 'removal_policies',
  72. 'removal_policies_mode'
  73. )
  74. _RESOURCE_DEF_KEYS = (
  75. RESOURCE_DEF_TYPE, RESOURCE_DEF_PROPERTIES, RESOURCE_DEF_METADATA,
  76. ) = (
  77. 'type', 'properties', 'metadata',
  78. )
  79. _REMOVAL_POLICIES_KEYS = (
  80. REMOVAL_RSRC_LIST,
  81. ) = (
  82. 'resource_list',
  83. )
  84. _REMOVAL_POLICY_MODES = (
  85. REMOVAL_POLICY_APPEND, REMOVAL_POLICY_UPDATE
  86. ) = (
  87. 'append', 'update'
  88. )
  89. _ROLLING_UPDATES_SCHEMA_KEYS = (
  90. MIN_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME,
  91. ) = (
  92. 'min_in_service', 'max_batch_size', 'pause_time',
  93. )
  94. _BATCH_CREATE_SCHEMA_KEYS = (
  95. MAX_BATCH_SIZE, PAUSE_TIME,
  96. ) = (
  97. 'max_batch_size', 'pause_time',
  98. )
  99. _UPDATE_POLICY_SCHEMA_KEYS = (
  100. ROLLING_UPDATE, BATCH_CREATE,
  101. ) = (
  102. 'rolling_update', 'batch_create',
  103. )
  104. ATTRIBUTES = (
  105. REFS, REFS_MAP, ATTR_ATTRIBUTES, REMOVED_RSRC_LIST
  106. ) = (
  107. 'refs', 'refs_map', 'attributes', 'removed_rsrc_list'
  108. )
  109. properties_schema = {
  110. COUNT: properties.Schema(
  111. properties.Schema.INTEGER,
  112. _('The number of resources to create.'),
  113. default=1,
  114. constraints=[
  115. constraints.Range(min=0),
  116. ],
  117. update_allowed=True
  118. ),
  119. INDEX_VAR: properties.Schema(
  120. properties.Schema.STRING,
  121. _('A variable that this resource will use to replace with the '
  122. 'current index of a given resource in the group. Can be used, '
  123. 'for example, to customize the name property of grouped '
  124. 'servers in order to differentiate them when listed with '
  125. 'nova client.'),
  126. default="%index%",
  127. constraints=[
  128. constraints.Length(min=3)
  129. ],
  130. support_status=support.SupportStatus(version='2014.2')
  131. ),
  132. RESOURCE_DEF: properties.Schema(
  133. properties.Schema.MAP,
  134. _('Resource definition for the resources in the group. The value '
  135. 'of this property is the definition of a resource just as if '
  136. 'it had been declared in the template itself.'),
  137. schema={
  138. RESOURCE_DEF_TYPE: properties.Schema(
  139. properties.Schema.STRING,
  140. _('The type of the resources in the group.'),
  141. required=True
  142. ),
  143. RESOURCE_DEF_PROPERTIES: properties.Schema(
  144. properties.Schema.MAP,
  145. _('Property values for the resources in the group.')
  146. ),
  147. RESOURCE_DEF_METADATA: properties.Schema(
  148. properties.Schema.MAP,
  149. _('Supplied metadata for the resources in the group.'),
  150. support_status=support.SupportStatus(version='5.0.0')
  151. ),
  152. },
  153. required=True,
  154. update_allowed=True
  155. ),
  156. REMOVAL_POLICIES: properties.Schema(
  157. properties.Schema.LIST,
  158. _('Policies for removal of resources on update.'),
  159. schema=properties.Schema(
  160. properties.Schema.MAP,
  161. _('Policy to be processed when doing an update which '
  162. 'requires removal of specific resources.'),
  163. schema={
  164. REMOVAL_RSRC_LIST: properties.Schema(
  165. properties.Schema.LIST,
  166. _("List of resources to be removed "
  167. "when doing an update which requires removal of "
  168. "specific resources. "
  169. "The resource may be specified several ways: "
  170. "(1) The resource name, as in the nested stack, "
  171. "(2) The resource reference returned from "
  172. "get_resource in a template, as available via "
  173. "the 'refs' attribute. "
  174. "Note this is destructive on update when specified; "
  175. "even if the count is not being reduced, and once "
  176. "a resource name is removed, its name is never "
  177. "reused in subsequent updates."
  178. ),
  179. default=[]
  180. ),
  181. },
  182. ),
  183. update_allowed=True,
  184. default=[],
  185. support_status=support.SupportStatus(version='2015.1')
  186. ),
  187. REMOVAL_POLICIES_MODE: properties.Schema(
  188. properties.Schema.STRING,
  189. _('How to handle changes to removal_policies on update. '
  190. 'The default "append" mode appends to the internal list, '
  191. '"update" replaces it on update.'),
  192. default=REMOVAL_POLICY_APPEND,
  193. constraints=[
  194. constraints.AllowedValues(_REMOVAL_POLICY_MODES)
  195. ],
  196. update_allowed=True,
  197. support_status=support.SupportStatus(version='10.0.0')
  198. ),
  199. }
  200. attributes_schema = {
  201. REFS: attributes.Schema(
  202. _("A list of resource IDs for the resources in the group."),
  203. type=attributes.Schema.LIST
  204. ),
  205. REFS_MAP: attributes.Schema(
  206. _("A map of resource names to IDs for the resources in "
  207. "the group."),
  208. type=attributes.Schema.MAP,
  209. support_status=support.SupportStatus(version='7.0.0'),
  210. ),
  211. ATTR_ATTRIBUTES: attributes.Schema(
  212. _("A map of resource names to the specified attribute of each "
  213. "individual resource. "
  214. "Requires heat_template_version: 2014-10-16."),
  215. support_status=support.SupportStatus(version='2014.2'),
  216. type=attributes.Schema.MAP
  217. ),
  218. REMOVED_RSRC_LIST: attributes.Schema(
  219. _("A list of removed resource names."),
  220. support_status=support.SupportStatus(version='7.0.0'),
  221. type=attributes.Schema.LIST
  222. ),
  223. }
  224. rolling_update_schema = {
  225. MIN_IN_SERVICE: properties.Schema(
  226. properties.Schema.INTEGER,
  227. _('The minimum number of resources in service while '
  228. 'rolling updates are being executed.'),
  229. constraints=[constraints.Range(min=0)],
  230. default=0),
  231. MAX_BATCH_SIZE: properties.Schema(
  232. properties.Schema.INTEGER,
  233. _('The maximum number of resources to replace at once.'),
  234. constraints=[constraints.Range(min=1)],
  235. default=1),
  236. PAUSE_TIME: properties.Schema(
  237. properties.Schema.NUMBER,
  238. _('The number of seconds to wait between batches of '
  239. 'updates.'),
  240. constraints=[constraints.Range(min=0)],
  241. default=0),
  242. }
  243. batch_create_schema = {
  244. MAX_BATCH_SIZE: properties.Schema(
  245. properties.Schema.INTEGER,
  246. _('The maximum number of resources to create at once.'),
  247. constraints=[constraints.Range(min=1)],
  248. default=1
  249. ),
  250. PAUSE_TIME: properties.Schema(
  251. properties.Schema.NUMBER,
  252. _('The number of seconds to wait between batches.'),
  253. constraints=[constraints.Range(min=0)],
  254. default=0
  255. ),
  256. }
  257. update_policy_schema = {
  258. ROLLING_UPDATE: properties.Schema(
  259. properties.Schema.MAP,
  260. schema=rolling_update_schema,
  261. support_status=support.SupportStatus(version='5.0.0')
  262. ),
  263. BATCH_CREATE: properties.Schema(
  264. properties.Schema.MAP,
  265. schema=batch_create_schema,
  266. support_status=support.SupportStatus(version='5.0.0')
  267. )
  268. }
  269. def get_size(self):
  270. return self.properties.get(self.COUNT)
  271. def validate_nested_stack(self):
  272. # Only validate the resource definition (which may be a
  273. # nested template) if count is non-zero, to enable folks
  274. # to disable features via a zero count if they wish
  275. if not self.get_size():
  276. return
  277. first_name = next(self._resource_names())
  278. test_tmpl = self._assemble_nested([first_name],
  279. include_all=True)
  280. res_def = next(iter(test_tmpl.resource_definitions(None).values()))
  281. # make sure we can resolve the nested resource type
  282. self.stack.env.get_class_to_instantiate(res_def.resource_type)
  283. try:
  284. name = "%s-%s" % (self.stack.name, self.name)
  285. nested_stack = self._parse_nested_stack(
  286. name,
  287. test_tmpl,
  288. self.child_params())
  289. nested_stack.strict_validate = False
  290. nested_stack.validate()
  291. except Exception as ex:
  292. path = "%s<%s>" % (self.name, self.template_url)
  293. raise exception.StackValidationFailed(
  294. ex, path=[self.stack.t.RESOURCES, path])
  295. def _current_skiplist(self):
  296. db_rsrc_names = self.data().get('name_blacklist')
  297. if db_rsrc_names:
  298. return db_rsrc_names.split(',')
  299. else:
  300. return []
  301. def _get_new_skiplist_entries(self, properties, current_skiplist):
  302. insp = grouputils.GroupInspector.from_parent_resource(self)
  303. # Now we iterate over the removal policies, and update the skiplist
  304. # with any additional names
  305. for r in properties.get(self.REMOVAL_POLICIES, []):
  306. if self.REMOVAL_RSRC_LIST in r:
  307. # Tolerate string or int list values
  308. for n in r[self.REMOVAL_RSRC_LIST]:
  309. str_n = str(n)
  310. if (str_n in current_skiplist or
  311. self.resource_id is None or
  312. str_n in insp.member_names(include_failed=True)):
  313. yield str_n
  314. elif isinstance(n, str):
  315. try:
  316. refids = self.get_output(self.REFS_MAP)
  317. except (exception.NotFound,
  318. exception.TemplateOutputError) as op_err:
  319. LOG.debug('Falling back to resource_by_refid() '
  320. ' due to %s', op_err)
  321. rsrc = self.nested().resource_by_refid(n)
  322. if rsrc is not None:
  323. yield rsrc.name
  324. else:
  325. if refids is not None:
  326. for name, refid in refids.items():
  327. if refid == n:
  328. yield name
  329. break
  330. # Clear output cache from prior to stack update, so we don't get
  331. # outdated values after stack update.
  332. self._outputs = None
  333. def _update_name_skiplist(self, properties):
  334. """Resolve the remove_policies to names for removal."""
  335. # To avoid reusing names after removal, we store a comma-separated
  336. # skiplist in the resource data - in cases where you want to
  337. # overwrite the stored data, removal_policies_mode: update can be used
  338. curr_sl = set(self._current_skiplist())
  339. p_mode = properties.get(self.REMOVAL_POLICIES_MODE,
  340. self.REMOVAL_POLICY_APPEND)
  341. if p_mode == self.REMOVAL_POLICY_UPDATE:
  342. init_sl = set()
  343. else:
  344. init_sl = curr_sl
  345. updated_sl = init_sl | set(self._get_new_skiplist_entries(properties,
  346. curr_sl))
  347. # If the skiplist has changed, update the resource data
  348. if updated_sl != curr_sl:
  349. self.data_set('name_blacklist', ','.join(sorted(updated_sl)))
  350. def _name_skiplist(self):
  351. """Get the list of resource names to skiplist."""
  352. sl = set(self._current_skiplist())
  353. if self.resource_id is None:
  354. sl |= set(self._get_new_skiplist_entries(self.properties, sl))
  355. return sl
  356. def _resource_names(self, size=None):
  357. name_skiplist = self._name_skiplist()
  358. if size is None:
  359. size = self.get_size()
  360. def is_skipped(name):
  361. return name in name_skiplist
  362. candidates = map(str, itertools.count())
  363. return itertools.islice(itertools.filterfalse(is_skipped,
  364. candidates),
  365. size)
  366. def _count_skipped(self, existing_members):
  367. """Return the number of current resource names that are skipped."""
  368. return len(self._name_skiplist() & set(existing_members))
  369. def handle_create(self):
  370. self._update_name_skiplist(self.properties)
  371. if self.update_policy.get(self.BATCH_CREATE) and self.get_size():
  372. batch_create = self.update_policy[self.BATCH_CREATE]
  373. max_batch_size = batch_create[self.MAX_BATCH_SIZE]
  374. pause_sec = batch_create[self.PAUSE_TIME]
  375. checkers = self._replace(0, max_batch_size, pause_sec)
  376. if checkers:
  377. checkers[0].start()
  378. return checkers
  379. else:
  380. names = self._resource_names()
  381. self.create_with_template(self._assemble_nested(names),
  382. self.child_params())
  383. def check_create_complete(self, checkers=None):
  384. if checkers is None:
  385. return super(ResourceGroup, self).check_create_complete()
  386. for checker in checkers:
  387. if not checker.started():
  388. checker.start()
  389. if not checker.step():
  390. return False
  391. return True
  392. def _run_to_completion(self, template, timeout):
  393. updater = self.update_with_template(template, {},
  394. timeout)
  395. while not super(ResourceGroup,
  396. self).check_update_complete(updater):
  397. yield
  398. def _run_update(self, total_capacity, max_updates, timeout):
  399. template = self._assemble_for_rolling_update(total_capacity,
  400. max_updates)
  401. return self._run_to_completion(template, timeout)
  402. def check_update_complete(self, checkers):
  403. for checker in checkers:
  404. if not checker.started():
  405. checker.start()
  406. if not checker.step():
  407. return False
  408. return True
  409. def res_def_changed(self, prop_diff):
  410. return self.RESOURCE_DEF in prop_diff
  411. def handle_update(self, json_snippet, tmpl_diff, prop_diff):
  412. if tmpl_diff:
  413. # parse update policy
  414. if tmpl_diff.update_policy_changed():
  415. up = json_snippet.update_policy(self.update_policy_schema,
  416. self.context)
  417. self.update_policy = up
  418. checkers = []
  419. self.properties = json_snippet.properties(self.properties_schema,
  420. self.context)
  421. self._update_name_skiplist(self.properties)
  422. if prop_diff and self.res_def_changed(prop_diff):
  423. updaters = self._try_rolling_update()
  424. if updaters:
  425. checkers.extend(updaters)
  426. if not checkers:
  427. resizer = scheduler.TaskRunner(
  428. self._run_to_completion,
  429. self._assemble_nested(self._resource_names()),
  430. self.stack.timeout_mins)
  431. checkers.append(resizer)
  432. checkers[0].start()
  433. return checkers
  434. def _attribute_output_name(self, *attr_path):
  435. if attr_path[0] == self.REFS:
  436. return self.REFS
  437. return ', '.join(str(a) for a in attr_path)
  438. def get_attribute(self, key, *path):
  439. if key == self.REMOVED_RSRC_LIST:
  440. return self._current_skiplist()
  441. if key == self.ATTR_ATTRIBUTES and not path:
  442. raise exception.InvalidTemplateAttribute(resource=self.name,
  443. key=key)
  444. is_resource_ref = (key.startswith("resource.") and
  445. not path and (len(key.split('.', 2)) == 2))
  446. if is_resource_ref:
  447. output_name = self.REFS_MAP
  448. else:
  449. output_name = self._attribute_output_name(key, *path)
  450. if self.resource_id is not None:
  451. try:
  452. output = self.get_output(output_name)
  453. except (exception.NotFound,
  454. exception.TemplateOutputError) as op_err:
  455. LOG.debug('Falling back to grouputils due to %s', op_err)
  456. else:
  457. if is_resource_ref:
  458. try:
  459. target = key.split('.', 2)[1]
  460. return output[target]
  461. except KeyError:
  462. raise exception.NotFound(_("Member '%(mem)s' not "
  463. "found in group resource "
  464. "'%(grp)s'.") %
  465. {'mem': target,
  466. 'grp': self.name})
  467. if key == self.REFS:
  468. return attributes.select_from_attribute(output, path)
  469. return output
  470. if key.startswith("resource."):
  471. return grouputils.get_nested_attrs(self, key, False, *path)
  472. names = self._resource_names()
  473. if key == self.REFS:
  474. vals = [grouputils.get_rsrc_id(self, key, False, n) for n in names]
  475. return attributes.select_from_attribute(vals, path)
  476. if key == self.REFS_MAP:
  477. refs_map = {n: grouputils.get_rsrc_id(self, key, False, n)
  478. for n in names}
  479. return refs_map
  480. if key == self.ATTR_ATTRIBUTES:
  481. return dict((n, grouputils.get_rsrc_attr(
  482. self, key, False, n, *path)) for n in names)
  483. path = [key] + list(path)
  484. return [grouputils.get_rsrc_attr(self, key, False, n, *path)
  485. for n in names]
  486. def _nested_output_defns(self, resource_names, get_attr_fn, get_res_fn):
  487. for attr in self.referenced_attrs():
  488. if isinstance(attr, str):
  489. key, path = attr, []
  490. else:
  491. key, path = attr[0], list(attr[1:])
  492. output_name = self._attribute_output_name(key, *path)
  493. value = None
  494. if key.startswith("resource."):
  495. keycomponents = key.split('.', 2)
  496. res_name = keycomponents[1]
  497. attr_path = keycomponents[2:] + path
  498. if attr_path:
  499. if res_name in resource_names:
  500. value = get_attr_fn([res_name] + attr_path)
  501. else:
  502. output_name = key = self.REFS_MAP
  503. elif key == self.ATTR_ATTRIBUTES and path:
  504. value = {r: get_attr_fn([r] + path) for r in resource_names}
  505. elif key not in self.ATTRIBUTES:
  506. value = [get_attr_fn([r, key] + path) for r in resource_names]
  507. if key == self.REFS:
  508. value = [get_res_fn(r) for r in resource_names]
  509. if value is not None:
  510. yield output.OutputDefinition(output_name, value)
  511. value = {r: get_res_fn(r) for r in resource_names}
  512. yield output.OutputDefinition(self.REFS_MAP, value)
  513. def build_resource_definition(self, res_name, res_defn):
  514. res_def = copy.deepcopy(res_defn)
  515. props = res_def.get(self.RESOURCE_DEF_PROPERTIES)
  516. if props:
  517. props = self._handle_repl_val(res_name, props)
  518. res_type = res_def[self.RESOURCE_DEF_TYPE]
  519. meta = res_def[self.RESOURCE_DEF_METADATA]
  520. return rsrc_defn.ResourceDefinition(res_name, res_type, props, meta)
  521. def get_resource_def(self, include_all=False):
  522. """Returns the resource definition portion of the group.
  523. :param include_all: if False, only properties for the resource
  524. definition that are not empty will be included
  525. :type include_all: bool
  526. :return: resource definition for the group
  527. :rtype: dict
  528. """
  529. # At this stage, we don't mind if all of the parameters have values
  530. # assigned. Pass in a custom resolver to the properties to not
  531. # error when a parameter does not have a user entered value.
  532. def ignore_param_resolve(snippet, nullable=False):
  533. if isinstance(snippet, function.Function):
  534. try:
  535. result = snippet.result()
  536. except exception.UserParameterMissing:
  537. return None
  538. if not (nullable or function._non_null_value(result)):
  539. result = None
  540. return result
  541. if isinstance(snippet, collections.Mapping):
  542. return dict(filter(function._non_null_item,
  543. ((k, ignore_param_resolve(v, nullable=True))
  544. for k, v in snippet.items())))
  545. elif (not isinstance(snippet, str) and
  546. isinstance(snippet, collections.Iterable)):
  547. return list(filter(function._non_null_value,
  548. (ignore_param_resolve(v, nullable=True)
  549. for v in snippet)))
  550. return snippet
  551. self.properties.resolve = ignore_param_resolve
  552. res_def = self.properties[self.RESOURCE_DEF]
  553. if not include_all:
  554. return self._clean_props(res_def)
  555. return res_def
  556. def _clean_props(self, res_defn):
  557. res_def = copy.deepcopy(res_defn)
  558. props = res_def.get(self.RESOURCE_DEF_PROPERTIES)
  559. if props:
  560. clean = dict((k, v) for k, v in props.items() if v is not None)
  561. props = clean
  562. res_def[self.RESOURCE_DEF_PROPERTIES] = props
  563. return res_def
  564. def _handle_repl_val(self, res_name, val):
  565. repl_var = self.properties[self.INDEX_VAR]
  566. def recurse(x):
  567. return self._handle_repl_val(res_name, x)
  568. if isinstance(val, str):
  569. return val.replace(repl_var, res_name)
  570. elif isinstance(val, collections.Mapping):
  571. return {k: recurse(v) for k, v in val.items()}
  572. elif isinstance(val, collections.Sequence):
  573. return [recurse(v) for v in val]
  574. return val
  575. def _add_output_defns_to_template(self, tmpl, resource_names):
  576. att_func = 'get_attr'
  577. get_attr = functools.partial(tmpl.functions[att_func], None, att_func)
  578. res_func = 'get_resource'
  579. get_res = functools.partial(tmpl.functions[res_func], None, res_func)
  580. for odefn in self._nested_output_defns(resource_names,
  581. get_attr, get_res):
  582. tmpl.add_output(odefn)
  583. def _assemble_nested(self, names, include_all=False,
  584. template_version=('heat_template_version',
  585. '2015-04-30')):
  586. def_dict = self.get_resource_def(include_all)
  587. definitions = [(k, self.build_resource_definition(k, def_dict))
  588. for k in names]
  589. tmpl = scl_template.make_template(definitions,
  590. version=template_version)
  591. self._add_output_defns_to_template(tmpl, [k for k, d in definitions])
  592. return tmpl
  593. def child_template_files(self, child_env):
  594. is_rolling_update = (self.action == self.UPDATE
  595. and self.update_policy[self.ROLLING_UPDATE])
  596. return grouputils.get_child_template_files(self.context,
  597. self.stack,
  598. is_rolling_update,
  599. self.old_template_id)
  600. def _assemble_for_rolling_update(self, total_capacity, max_updates,
  601. include_all=False,
  602. template_version=('heat_template_version',
  603. '2015-04-30')):
  604. names = list(self._resource_names(total_capacity))
  605. name_skiplist = self._name_skiplist()
  606. valid_resources = [(n, d) for n, d in
  607. grouputils.get_member_definitions(self)
  608. if n not in name_skiplist]
  609. targ_cap = self.get_size()
  610. def replace_priority(res_item):
  611. name, defn = res_item
  612. try:
  613. index = names.index(name)
  614. except ValueError:
  615. # High priority - delete immediately
  616. return 0
  617. else:
  618. if index < targ_cap:
  619. # Update higher indices first
  620. return targ_cap - index
  621. else:
  622. # Low priority - don't update
  623. return total_capacity
  624. old_resources = sorted(valid_resources, key=replace_priority)
  625. existing_names = set(n for n, d in valid_resources)
  626. new_names = itertools.filterfalse(lambda n: n in existing_names,
  627. names)
  628. res_def = self.get_resource_def(include_all)
  629. definitions = scl_template.member_definitions(
  630. old_resources, res_def,
  631. total_capacity,
  632. max_updates,
  633. lambda: next(new_names),
  634. self.build_resource_definition)
  635. tmpl = scl_template.make_template(definitions,
  636. version=template_version)
  637. self._add_output_defns_to_template(tmpl, names)
  638. return tmpl
  639. def _try_rolling_update(self):
  640. if self.update_policy[self.ROLLING_UPDATE]:
  641. policy = self.update_policy[self.ROLLING_UPDATE]
  642. return self._replace(policy[self.MIN_IN_SERVICE],
  643. policy[self.MAX_BATCH_SIZE],
  644. policy[self.PAUSE_TIME])
  645. def _resolve_attribute(self, name):
  646. if name == self.REMOVED_RSRC_LIST:
  647. return self._current_skiplist()
  648. def _update_timeout(self, batch_cnt, pause_sec):
  649. total_pause_time = pause_sec * max(batch_cnt - 1, 0)
  650. if total_pause_time >= self.stack.timeout_secs():
  651. msg = _('The current update policy will result in stack update '
  652. 'timeout.')
  653. raise ValueError(msg)
  654. return self.stack.timeout_secs() - total_pause_time
  655. @staticmethod
  656. def _get_batches(targ_cap, curr_cap, batch_size, min_in_service):
  657. updated = 0
  658. while rolling_update.needs_update(targ_cap, curr_cap, updated):
  659. new_cap, total_new = rolling_update.next_batch(targ_cap,
  660. curr_cap,
  661. updated,
  662. batch_size,
  663. min_in_service)
  664. yield new_cap, total_new
  665. updated += total_new - max(new_cap - max(curr_cap, targ_cap), 0)
  666. curr_cap = new_cap
  667. def _replace(self, min_in_service, batch_size, pause_sec):
  668. def pause_between_batch(pause_sec):
  669. duration = timeutils.Duration(pause_sec)
  670. while not duration.expired():
  671. yield
  672. # current capacity not including existing skiplisted
  673. inspector = grouputils.GroupInspector.from_parent_resource(self)
  674. num_skiplist = self._count_skipped(
  675. inspector.member_names(include_failed=False))
  676. num_resources = inspector.size(include_failed=True)
  677. curr_cap = num_resources - num_skiplist
  678. batches = list(self._get_batches(self.get_size(), curr_cap, batch_size,
  679. min_in_service))
  680. update_timeout = self._update_timeout(len(batches), pause_sec)
  681. def tasks():
  682. for index, (curr_cap, max_upd) in enumerate(batches):
  683. yield scheduler.TaskRunner(self._run_update,
  684. curr_cap, max_upd,
  685. update_timeout)
  686. if index < (len(batches) - 1) and pause_sec > 0:
  687. yield scheduler.TaskRunner(pause_between_batch, pause_sec)
  688. return list(tasks())
  689. def preview(self):
  690. # NOTE(pas-ha) just need to use include_all in _assemble_nested,
  691. # so this method is a simplified copy of preview() from StackResource,
  692. # and next two lines are basically a modified copy of child_template()
  693. names = self._resource_names()
  694. child_template = self._assemble_nested(names, include_all=True)
  695. params = self.child_params()
  696. name = "%s-%s" % (self.stack.name, self.name)
  697. self._nested = self._parse_nested_stack(name, child_template, params)
  698. return self.nested().preview_resources()
  699. def child_template(self):
  700. names = self._resource_names()
  701. return self._assemble_nested(names)
  702. def child_params(self):
  703. return {}
  704. def handle_adopt(self, resource_data):
  705. names = self._resource_names()
  706. if names:
  707. return self.create_with_template(self._assemble_nested(names),
  708. {},
  709. adopt_data=resource_data)
  710. def get_nested_parameters_stack(self):
  711. """Return a nested group of size 1 for validation."""
  712. names = self._resource_names(1)
  713. child_template = self._assemble_nested(names)
  714. params = self.child_params()
  715. name = "%s-%s" % (self.stack.name, self.name)
  716. return self._parse_nested_stack(name, child_template, params)
  717. def resource_mapping():
  718. return {
  719. 'OS::Heat::ResourceGroup': ResourceGroup,
  720. }