Sahara Horizon plugin.
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.

create.py 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. # Licensed under the Apache License, Version 2.0 (the "License");
  2. # you may not use this file except in compliance with the License.
  3. # You may obtain a copy of the License at
  4. #
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. #
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  10. # implied.
  11. # See the License for the specific language governing permissions and
  12. # limitations under the License.
  13. import itertools
  14. import uuid
  15. from django.utils import encoding
  16. from django.utils import html
  17. from django.utils import safestring
  18. from django.utils.translation import ugettext_lazy as _
  19. from oslo_log import log as logging
  20. from saharaclient.api import base as api_base
  21. from horizon import exceptions
  22. from horizon import forms
  23. from horizon import workflows
  24. from openstack_dashboard.api import cinder
  25. from openstack_dashboard.api import network
  26. from openstack_dashboard.dashboards.project.instances \
  27. import utils as nova_utils
  28. from openstack_dashboard.dashboards.project.volumes \
  29. import utils as cinder_utils
  30. from sahara_dashboard.api import manila as manilaclient
  31. from sahara_dashboard.api import sahara as saharaclient
  32. from sahara_dashboard.content.data_processing.utils \
  33. import acl as acl_utils
  34. from sahara_dashboard.content.data_processing.utils \
  35. import helpers
  36. from sahara_dashboard.content.data_processing.utils \
  37. import workflow_helpers
  38. LOG = logging.getLogger(__name__)
  39. BASE_IMAGE_URL = "horizon:project:data_processing.clusters:register"
  40. def is_cinder_enabled(request):
  41. return saharaclient.base.is_service_enabled(request, 'volume')
  42. def storage_choices(request):
  43. choices = [("ephemeral_drive", _("Ephemeral Drive")), ]
  44. if is_cinder_enabled(request):
  45. choices.append(("cinder_volume", _("Cinder Volume")))
  46. else:
  47. LOG.warning(_("Cinder service is unavailable now"))
  48. return choices
  49. class GeneralConfigAction(workflows.Action):
  50. nodegroup_name = forms.CharField(label=_("Template Name"))
  51. description = forms.CharField(label=_("Description"),
  52. required=False,
  53. widget=forms.Textarea(attrs={'rows': 4}))
  54. flavor = forms.ChoiceField(label=_("OpenStack Flavor"))
  55. availability_zone = forms.ChoiceField(
  56. label=_("Availability Zone"),
  57. help_text=_("Launch instances in this availability zone."),
  58. required=False,
  59. widget=forms.Select(attrs={"class": "availability_zone_field"})
  60. )
  61. storage = forms.ChoiceField(
  62. label=_("Storage location"),
  63. help_text=_("Choose a storage location"),
  64. choices=[],
  65. widget=forms.Select(attrs={
  66. "class": "storage_field switchable",
  67. 'data-slug': 'storage_loc'
  68. }))
  69. volumes_per_node = forms.IntegerField(
  70. label=_("Volumes per node"),
  71. required=False,
  72. initial=1,
  73. widget=forms.TextInput(attrs={
  74. "class": "volume_per_node_field switched",
  75. "data-switch-on": "storage_loc",
  76. "data-storage_loc-cinder_volume": _('Volumes per node')
  77. })
  78. )
  79. volumes_size = forms.IntegerField(
  80. label=_("Volumes size (GB)"),
  81. required=False,
  82. initial=10,
  83. widget=forms.TextInput(attrs={
  84. "class": "volume_size_field switched",
  85. "data-switch-on": "storage_loc",
  86. "data-storage_loc-cinder_volume": _('Volumes size (GB)')
  87. })
  88. )
  89. volume_type = forms.ChoiceField(
  90. label=_("Volumes type"),
  91. required=False,
  92. widget=forms.Select(attrs={
  93. "class": "volume_type_field switched",
  94. "data-switch-on": "storage_loc",
  95. "data-storage_loc-cinder_volume": _('Volumes type')
  96. })
  97. )
  98. volume_local_to_instance = forms.BooleanField(
  99. label=_("Volume local to instance"),
  100. required=False,
  101. help_text=_("Instance and attached volumes will be created on the "
  102. "same physical host"),
  103. widget=forms.CheckboxInput(attrs={
  104. "class": "volume_local_to_instance_field switched",
  105. "data-switch-on": "storage_loc",
  106. "data-storage_loc-cinder_volume": _('Volume local to instance')
  107. })
  108. )
  109. volumes_availability_zone = forms.ChoiceField(
  110. label=_("Volumes Availability Zone"),
  111. help_text=_("Create volumes in this availability zone."),
  112. required=False,
  113. widget=forms.Select(attrs={
  114. "class": "volumes_availability_zone_field switched",
  115. "data-switch-on": "storage_loc",
  116. "data-storage_loc-cinder_volume": _('Volumes Availability Zone')
  117. })
  118. )
  119. image = forms.DynamicChoiceField(label=_("Base Image"),
  120. required=False,
  121. add_item_link=BASE_IMAGE_URL)
  122. hidden_configure_field = forms.CharField(
  123. required=False,
  124. widget=forms.HiddenInput(attrs={"class": "hidden_configure_field"}))
  125. def __init__(self, request, *args, **kwargs):
  126. super(GeneralConfigAction, self).__init__(request, *args, **kwargs)
  127. hlps = helpers.Helpers(request)
  128. plugin, hadoop_version = (
  129. workflow_helpers.get_plugin_and_hadoop_version(request))
  130. if not saharaclient.SAHARA_AUTO_IP_ALLOCATION_ENABLED:
  131. pools = network.floating_ip_pools_list(request)
  132. pool_choices = [(pool.id, pool.name) for pool in pools]
  133. pool_choices.insert(0, (None, "Do not assign floating IPs"))
  134. self.fields['floating_ip_pool'] = forms.ChoiceField(
  135. label=_("Floating IP Pool"),
  136. choices=pool_choices,
  137. required=False)
  138. self.fields["use_autoconfig"] = forms.BooleanField(
  139. label=_("Auto-configure"),
  140. help_text=_("If selected, instances of a node group will be "
  141. "automatically configured during cluster "
  142. "creation. Otherwise you should manually specify "
  143. "configuration values."),
  144. required=False,
  145. widget=forms.CheckboxInput(),
  146. initial=True,
  147. )
  148. self.fields["proxygateway"] = forms.BooleanField(
  149. label=_("Proxy Gateway"),
  150. widget=forms.CheckboxInput(),
  151. help_text=_("Sahara will use instances of this node group to "
  152. "access other cluster instances."),
  153. required=False)
  154. self.fields['is_public'] = acl_utils.get_is_public_form(
  155. _("node group template"))
  156. self.fields['is_protected'] = acl_utils.get_is_protected_form(
  157. _("node group template"))
  158. self.fields["plugin_name"] = forms.CharField(
  159. widget=forms.HiddenInput(),
  160. initial=plugin
  161. )
  162. self.fields["hadoop_version"] = forms.CharField(
  163. widget=forms.HiddenInput(),
  164. initial=hadoop_version
  165. )
  166. self.fields["storage"].choices = storage_choices(request)
  167. node_parameters = hlps.get_general_node_group_configs(plugin,
  168. hadoop_version)
  169. for param in node_parameters:
  170. self.fields[param.name] = workflow_helpers.build_control(param)
  171. # when we copy or edit a node group template then
  172. # request contains valuable info in both GET and POST methods
  173. req = request.GET.copy()
  174. req.update(request.POST)
  175. if req.get("guide_template_type"):
  176. self.fields["guide_template_type"] = forms.CharField(
  177. required=False,
  178. widget=forms.HiddenInput(),
  179. initial=req.get("guide_template_type"))
  180. if is_cinder_enabled(request):
  181. volume_types = cinder.volume_type_list(request)
  182. else:
  183. volume_types = []
  184. self.fields['volume_type'].choices = [(None, _("No volume type"))] + \
  185. [(type.name, type.name)
  186. for type in volume_types]
  187. def populate_flavor_choices(self, request, context):
  188. flavors = nova_utils.flavor_list(request)
  189. if flavors:
  190. return nova_utils.sort_flavor_list(request, flavors)
  191. return []
  192. def populate_availability_zone_choices(self, request, context):
  193. # The default is None, i.e. not specifying any availability zone
  194. az_list = [(None, _('No availability zone specified'))]
  195. az_list.extend([(az.zoneName, az.zoneName)
  196. for az in nova_utils.availability_zone_list(request)
  197. if az.zoneState['available']])
  198. return az_list
  199. def populate_volumes_availability_zone_choices(self, request, context):
  200. az_list = [(None, _('No availability zone specified'))]
  201. if is_cinder_enabled(request):
  202. az_list.extend([(az.zoneName, az.zoneName)
  203. for az in cinder_utils.availability_zone_list(
  204. request) if az.zoneState['available']])
  205. return az_list
  206. def populate_image_choices(self, request, context):
  207. return workflow_helpers.populate_image_choices(self, request, context,
  208. empty_choice=True)
  209. def get_help_text(self):
  210. extra = dict()
  211. plugin_name, hadoop_version = (
  212. workflow_helpers.get_plugin_and_hadoop_version(self.request))
  213. extra["plugin_name"] = plugin_name
  214. extra["hadoop_version"] = hadoop_version
  215. plugin = saharaclient.plugin_get_version_details(
  216. self.request, plugin_name, hadoop_version)
  217. extra["deprecated"] = workflow_helpers.is_version_of_plugin_deprecated(
  218. plugin, hadoop_version)
  219. return super(GeneralConfigAction, self).get_help_text(extra)
  220. class Meta(object):
  221. name = _("Configure Node Group Template")
  222. help_text_template = "nodegroup_templates/_configure_general_help.html"
  223. class SecurityConfigAction(workflows.Action):
  224. def __init__(self, request, *args, **kwargs):
  225. super(SecurityConfigAction, self).__init__(request, *args, **kwargs)
  226. self.fields["security_autogroup"] = forms.BooleanField(
  227. label=_("Auto Security Group"),
  228. widget=forms.CheckboxInput(),
  229. help_text=_("Create security group for this Node Group."),
  230. required=False,
  231. initial=True)
  232. try:
  233. groups = network.security_group_list(request)
  234. except Exception:
  235. exceptions.handle(request,
  236. _("Unable to get security group list."))
  237. raise
  238. security_group_list = [(sg.id, sg.name) for sg in groups]
  239. self.fields["security_groups"] = forms.MultipleChoiceField(
  240. label=_("Security Groups"),
  241. widget=forms.CheckboxSelectMultiple(),
  242. help_text=_("Launch instances in these security groups."),
  243. choices=security_group_list,
  244. required=False)
  245. class Meta(object):
  246. name = _("Security")
  247. help_text = _("Control access to instances of the node group.")
  248. class CheckboxSelectMultiple(forms.CheckboxSelectMultiple):
  249. def render(self, name, value, attrs=None, choices=()):
  250. if value is None:
  251. value = []
  252. has_id = attrs and 'id' in attrs
  253. final_attrs = self.build_attrs(attrs, name=name)
  254. output = []
  255. initial_service = uuid.uuid4()
  256. str_values = set([encoding.force_text(v) for v in value])
  257. for i, (option_value, option_label) in enumerate(
  258. itertools.chain(self.choices, choices)):
  259. current_service = option_value.split(':')[0]
  260. if current_service != initial_service:
  261. if i > 0:
  262. output.append("</ul>")
  263. service_description = _("%s processes: ") % current_service
  264. service_description = html.conditional_escape(
  265. encoding.force_text(service_description))
  266. output.append("<label>%s</label>" % service_description)
  267. initial_service = current_service
  268. output.append(encoding.force_text("<ul>"))
  269. if has_id:
  270. final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
  271. label_for = ' for="%s"' % final_attrs['id']
  272. else:
  273. label_for = ''
  274. cb = forms.CheckboxInput(
  275. final_attrs, check_test=lambda value: value in str_values)
  276. option_value = encoding.force_text(option_value)
  277. rendered_cb = cb.render(name, option_value)
  278. option_label = html.conditional_escape(
  279. encoding.force_text(option_label))
  280. output.append(
  281. '<li><label%s>%s %s</label></li>' %
  282. (label_for, rendered_cb, option_label))
  283. output.append('</ul>')
  284. return safestring.mark_safe('\n'.join(output))
  285. class SelectNodeProcessesAction(workflows.Action):
  286. def __init__(self, request, *args, **kwargs):
  287. super(SelectNodeProcessesAction, self).__init__(
  288. request, *args, **kwargs)
  289. plugin, hadoop_version = (
  290. workflow_helpers.get_plugin_and_hadoop_version(request))
  291. node_processes = {}
  292. try:
  293. version_details = saharaclient.plugin_get_version_details(
  294. request, plugin, hadoop_version)
  295. node_processes = version_details.node_processes
  296. except Exception:
  297. exceptions.handle(request,
  298. _("Unable to generate process choices."))
  299. process_choices = []
  300. for service, processes in node_processes.items():
  301. for process in processes:
  302. choice_label = str(service) + ":" + str(process)
  303. process_choices.append((choice_label, process))
  304. self.fields["processes"] = forms.MultipleChoiceField(
  305. label=_("Select Node Group Processes"),
  306. widget=CheckboxSelectMultiple(),
  307. choices=process_choices,
  308. required=True)
  309. class Meta(object):
  310. name = _("Node Processes")
  311. help_text = _("Select node processes for the node group")
  312. class SelectNodeGroupSharesAction(workflows.Action):
  313. def __init__(self, request, *args, **kwargs):
  314. super(SelectNodeGroupSharesAction, self).__init__(
  315. request, *args, **kwargs)
  316. possible_shares = self.get_possible_shares(request)
  317. self.fields["shares"] = workflow_helpers.MultipleShareChoiceField(
  318. label=_("Select Shares"),
  319. widget=workflow_helpers.ShareWidget(choices=possible_shares),
  320. required=False,
  321. choices=possible_shares
  322. )
  323. def get_possible_shares(self, request):
  324. try:
  325. shares = manilaclient.share_list(request)
  326. choices = [(s.id, s.name) for s in shares]
  327. except Exception:
  328. exceptions.handle(request, _("Failed to get list of shares"))
  329. choices = []
  330. return choices
  331. class Meta(object):
  332. name = _("Shares")
  333. help_text = _("Select the manila shares for this node group")
  334. class GeneralConfig(workflows.Step):
  335. action_class = GeneralConfigAction
  336. contributes = ("general_nodegroup_name", )
  337. def contribute(self, data, context):
  338. for k, v in data.items():
  339. if "hidden" in k:
  340. continue
  341. context["general_" + k] = v if v != "None" else None
  342. return context
  343. class SecurityConfig(workflows.Step):
  344. action_class = SecurityConfigAction
  345. contributes = ("security_autogroup", "security_groups")
  346. class SelectNodeProcesses(workflows.Step):
  347. action_class = SelectNodeProcessesAction
  348. def contribute(self, data, context):
  349. post = self.workflow.request.POST
  350. context['general_processes'] = post.getlist('processes')
  351. return context
  352. class SelectNodeGroupShares(workflows.Step):
  353. action_class = SelectNodeGroupSharesAction
  354. def contribute(self, data, context):
  355. post = self.workflow.request.POST
  356. shares_details = []
  357. for index in range(0, len(self.action.fields['shares'].choices) * 3):
  358. if index % 3 == 0:
  359. share = post.get("shares_{0}".format(index))
  360. if share:
  361. path = post.get("shares_{0}".format(index + 1))
  362. permissions = post.get("shares_{0}".format(index + 2))
  363. shares_details.append({
  364. "id": share,
  365. "path": path,
  366. "access_level": permissions
  367. })
  368. context['ngt_shares'] = shares_details
  369. return context
  370. class ConfigureNodegroupTemplate(workflow_helpers.ServiceParametersWorkflow,
  371. workflow_helpers.StatusFormatMixin):
  372. slug = "configure_nodegroup_template"
  373. name = _("Create Node Group Template")
  374. finalize_button_name = _("Create")
  375. success_message = _("Created Node Group Template %s")
  376. name_property = "general_nodegroup_name"
  377. success_url = ("horizon:project:data_processing.clusters:"
  378. "nodegroup-templates-tab")
  379. default_steps = (GeneralConfig, SelectNodeProcesses, SecurityConfig, )
  380. def __init__(self, request, context_seed, entry_point, *args, **kwargs):
  381. hlps = helpers.Helpers(request)
  382. plugin, hadoop_version = (
  383. workflow_helpers.get_plugin_and_hadoop_version(request))
  384. general_parameters, service_parameters = \
  385. hlps.get_general_and_service_nodegroups_parameters(plugin,
  386. hadoop_version)
  387. if saharaclient.base.is_service_enabled(request, 'share'):
  388. ConfigureNodegroupTemplate._register_step(self,
  389. SelectNodeGroupShares)
  390. self._populate_tabs(general_parameters, service_parameters)
  391. super(ConfigureNodegroupTemplate, self).__init__(request,
  392. context_seed,
  393. entry_point,
  394. *args, **kwargs)
  395. def is_valid(self):
  396. missing = self.depends_on - set(self.context.keys())
  397. if missing:
  398. raise exceptions.WorkflowValidationError(
  399. "Unable to complete the workflow. The values %s are "
  400. "required but not present." % ", ".join(missing))
  401. checked_steps = []
  402. if "general_processes" in self.context:
  403. checked_steps = self.context["general_processes"]
  404. enabled_services = set([])
  405. for process_name in checked_steps:
  406. enabled_services.add(str(process_name).split(":")[0])
  407. steps_valid = True
  408. for step in self.steps:
  409. process_name = str(getattr(step, "process_name", None))
  410. if process_name not in enabled_services and \
  411. not isinstance(step, GeneralConfig):
  412. continue
  413. if not step.action.is_valid():
  414. steps_valid = False
  415. step.has_errors = True
  416. if not steps_valid:
  417. return steps_valid
  418. return self.validate(self.context)
  419. def handle(self, request, context):
  420. try:
  421. processes = []
  422. for service_process in context["general_processes"]:
  423. processes.append(str(service_process).split(":")[1])
  424. configs_dict = (
  425. workflow_helpers.parse_configs_from_context(
  426. context, self.defaults))
  427. plugin, hadoop_version = (
  428. workflow_helpers.get_plugin_and_hadoop_version(request))
  429. volumes_per_node = None
  430. volumes_size = None
  431. volumes_availability_zone = None
  432. volume_type = None
  433. volume_local_to_instance = False
  434. if context["general_storage"] == "cinder_volume":
  435. volumes_per_node = context["general_volumes_per_node"]
  436. volumes_size = context["general_volumes_size"]
  437. volumes_availability_zone = \
  438. context["general_volumes_availability_zone"]
  439. volume_type = context["general_volume_type"]
  440. volume_local_to_instance = \
  441. context["general_volume_local_to_instance"]
  442. ngt_shares = context.get('ngt_shares', [])
  443. image_id = context["general_image"] or None
  444. ngt = saharaclient.nodegroup_template_create(
  445. request,
  446. name=context["general_nodegroup_name"],
  447. plugin_name=plugin,
  448. hadoop_version=hadoop_version,
  449. description=context["general_description"],
  450. flavor_id=context["general_flavor"],
  451. volumes_per_node=volumes_per_node,
  452. volumes_size=volumes_size,
  453. volumes_availability_zone=volumes_availability_zone,
  454. volume_type=volume_type,
  455. volume_local_to_instance=volume_local_to_instance,
  456. node_processes=processes,
  457. node_configs=configs_dict,
  458. floating_ip_pool=context.get("general_floating_ip_pool"),
  459. security_groups=context["security_groups"],
  460. auto_security_group=context["security_autogroup"],
  461. is_proxy_gateway=context["general_proxygateway"],
  462. availability_zone=context["general_availability_zone"],
  463. use_autoconfig=context['general_use_autoconfig'],
  464. shares=ngt_shares,
  465. is_public=context['general_is_public'],
  466. is_protected=context['general_is_protected'],
  467. image_id=image_id)
  468. hlps = helpers.Helpers(request)
  469. if hlps.is_from_guide():
  470. guide_type = context["general_guide_template_type"]
  471. request.session[guide_type + "_name"] = (
  472. context["general_nodegroup_name"])
  473. request.session[guide_type + "_id"] = ngt.id
  474. self.success_url = (
  475. "horizon:project:data_processing.clusters:cluster_guide")
  476. return True
  477. except api_base.APIException as e:
  478. self.error_description = str(e)
  479. return False
  480. except Exception:
  481. exceptions.handle(request)
  482. class SelectPluginAction(workflows.Action,
  483. workflow_helpers.PluginAndVersionMixin):
  484. hidden_create_field = forms.CharField(
  485. required=False,
  486. widget=forms.HiddenInput(attrs={"class": "hidden_create_field"}))
  487. def __init__(self, request, *args, **kwargs):
  488. super(SelectPluginAction, self).__init__(request, *args, **kwargs)
  489. sahara = saharaclient.client(request)
  490. self._generate_plugin_version_fields(sahara)
  491. class Meta(object):
  492. name = _("Select plugin and hadoop version")
  493. help_text_template = "nodegroup_templates/_create_general_help.html"
  494. class SelectPlugin(workflows.Step):
  495. action_class = SelectPluginAction
  496. contributes = ("plugin_name", "hadoop_version")
  497. def contribute(self, data, context):
  498. context = super(SelectPlugin, self).contribute(data, context)
  499. context["plugin_name"] = data.get('plugin_name', None)
  500. context["hadoop_version"] = \
  501. data.get(context["plugin_name"] + "_version", None)
  502. return context
  503. class CreateNodegroupTemplate(workflows.Workflow):
  504. slug = "create_nodegroup_template"
  505. name = _("Create Node Group Template")
  506. finalize_button_name = _("Next")
  507. success_message = _("Created")
  508. failure_message = _("Could not create")
  509. success_url = ("horizon:project:data_processing.clusters:"
  510. "nodegroup-templates-tab")
  511. default_steps = (SelectPlugin,)