diff --git a/dashboard/api/windc.py b/dashboard/api/windc.py index 15aa289..a58d617 100644 --- a/dashboard/api/windc.py +++ b/dashboard/api/windc.py @@ -35,27 +35,35 @@ def windcclient(request): % (request.user.token, url)) return windc_client.Client(endpoint=url, token=None) + def datacenters_create(request, parameters): name = parameters.get('name', '') return windcclient(request).datacenters.create(name) + def datacenters_delete(request, datacenter_id): return windcclient(request).datacenters.delete(datacenter_id) + def datacenters_get(request, datacenter_id): return windcclient(request).datacenters.get(datacenter_id) + def datacenters_list(request): return windcclient(request).datacenters.list() + def services_create(request, datacenter, parameters): return windcclient(request).services.create(datacenter, parameters) + def services_list(request, datacenter): return windcclient(request).services.list(datacenter) + def services_get(request, datacenter, service_id): return windcclient(request).services.get(datacenter, service_id) + def services_delete(request, datacenter, service_id): return windcclient(request).services.delete(datacenter, service_id) diff --git a/dashboard/windc/forms.py b/dashboard/windc/forms.py index 7c1329f..80ba327 100644 --- a/dashboard/windc/forms.py +++ b/dashboard/windc/forms.py @@ -20,19 +20,30 @@ import logging +from django import forms from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from openstack_dashboard import api -from horizon import exceptions from horizon import forms +from horizon import exceptions from horizon import messages +import pdb LOG = logging.getLogger(__name__) +class WizardFormServiceType(forms.Form): + _type = forms.ChoiceField(label=_("Service Type")) + + +class WizardFormConfiguration(forms.Form): + subject = forms.CharField(max_length=100) + sender = forms.CharField(max_length=1) + + class UpdateWinDC(forms.SelfHandlingForm): tenant_id = forms.CharField(widget=forms.HiddenInput) data_center = forms.CharField(widget=forms.HiddenInput) diff --git a/dashboard/windc/tables.py b/dashboard/windc/tables.py index bf34d4b..85a1c8f 100644 --- a/dashboard/windc/tables.py +++ b/dashboard/windc/tables.py @@ -81,8 +81,7 @@ class DeleteDataCenter(tables.BatchAction): return True def action(self, request, datacenter_id): - datacenter = api.windc.datacenters_get(request, datacenter_id) - api.windc.datacenters_delete(request, datacenter) + api.windc.datacenters_delete(request, datacenter_id) class DeleteService(tables.BatchAction): @@ -101,9 +100,8 @@ class DeleteService(tables.BatchAction): link = request.__dict__['META']['HTTP_REFERER'] datacenter_id = re.search('windc/(\S+)', link).group(0)[6:-1] ############## - datacenter = api.windc.datacenters_get(request, datacenter_id) - - api.windc.services_delete(request, datacenter, service_id) + + api.windc.services_delete(request, datacenter_id, service_id) class EditService(tables.LinkAction): @@ -116,6 +114,16 @@ class EditService(tables.LinkAction): return True +class Wizard(tables.LinkAction): + name = "wizard" + verbose_name = _("Wizard") + url = "horizon:project:windc:update" + classes = ("ajax-modal", "btn-edit") + + def allowed(self, request, instance): + return True + + class ShowDataCenterServices(tables.LinkAction): name = "edit" verbose_name = _("Services") @@ -144,18 +152,36 @@ class WinDCTable(tables.DataTable): name = "windc" verbose_name = _("Windows Data Centers") row_class = UpdateRow - table_actions = (CreateDataCenter,) - row_actions = (ShowDataCenterServices,DeleteDataCenter) + table_actions = (CreateDataCenter, Wizard) + row_actions = (ShowDataCenterServices, DeleteDataCenter) + + +STATUS_DISPLAY_CHOICES = ( + ("create", "Deploy"), +) class WinServicesTable(tables.DataTable): - name = tables.Column('dc_name', verbose_name=_('Name')) + + STATUS_CHOICES = ( + (None, True), + ("deployed", True), + ("active", True), + ("error", False), + ) + + name = tables.Column('dc_name', verbose_name=_('Name'), + link=("horizon:project:windc:service_details"),) _type = tables.Column('type', verbose_name=_('Type')) - status = tables.Column('status', verbose_name=_('Status')) + status = tables.Column('status', verbose_name=_('Status'), + status=True, + status_choices=STATUS_CHOICES, + display_choices=STATUS_DISPLAY_CHOICES) class Meta: name = "services" verbose_name = _("Services") row_class = UpdateRow + status_columns = ['status'] table_actions = (CreateService,) row_actions = (EditService, DeleteService) diff --git a/dashboard/windc/templates/windc/_dc_help.html b/dashboard/windc/templates/windc/_dc_help.html index 1cb4efc..5e29e2d 100644 --- a/dashboard/windc/templates/windc/_dc_help.html +++ b/dashboard/windc/templates/windc/_dc_help.html @@ -1,2 +1,3 @@ {% load i18n %} -

{% blocktrans %}You can deploy few domain controllers with one name.{% endblocktrans %}

\ No newline at end of file +

{% blocktrans %}You can deploy few Active Directory services with one domain name.{% endblocktrans %}

+

{% blocktrans %}The DNS service will automatically created for each Active Directory.{% endblocktrans %}

\ No newline at end of file diff --git a/dashboard/windc/templates/windc/_services_tabs.html b/dashboard/windc/templates/windc/_services_tabs.html new file mode 100644 index 0000000..9839711 --- /dev/null +++ b/dashboard/windc/templates/windc/_services_tabs.html @@ -0,0 +1,28 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-header %}{% trans "Create Service" %}{% endblock %} + +{% block modal-body %} +

Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}

+
{% csrf_token %} + +{{ wizard.management_form }} +{% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form }} + {% endfor %} +{% else %} + {{ wizard.form }} +{% endif %} +{{ wizard.form.forms }} +
+{% if wizard.steps.prev %} + + +{% else %} + + +{% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/dashboard/windc/urls.py b/dashboard/windc/urls.py index 6654ed2..faa54d9 100644 --- a/dashboard/windc/urls.py +++ b/dashboard/windc/urls.py @@ -20,8 +20,10 @@ from django.conf.urls.defaults import patterns, url -from .views import IndexView, CreateWinDCView, WinServices, CreateWinServiceView - +from .views import IndexView, WinServices, \ + CreateWinDCView, CreateWinServiceView +from .views import Wizard +from .forms import WizardFormServiceType, WizardFormConfiguration VIEW_MOD = 'openstack_dashboard.dashboards.project.windc.views' @@ -29,6 +31,11 @@ urlpatterns = patterns(VIEW_MOD, url(r'^$', IndexView.as_view(), name='index'), url(r'^create$', CreateWinServiceView.as_view(), name='create'), url(r'^create_dc$', CreateWinDCView.as_view(), name='create_dc'), - url(r'^(?P[^/]+)/$', WinServices.as_view(), - name='services') + url(r'^(?P[^/]+)/$', WinServices.as_view(), + name='services'), + url(r'^update$', + Wizard.as_view([WizardFormServiceType, WizardFormConfiguration]), + name='update'), + url(r'^(?P[^/]+)/$', WinServices.as_view(), + name='service_details') ) diff --git a/dashboard/windc/views.py b/dashboard/windc/views.py index 9c34c29..87595b2 100644 --- a/dashboard/windc/views.py +++ b/dashboard/windc/views.py @@ -15,7 +15,8 @@ # 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. +# License for the specific language governing permissions and limitations +# under the License. """ Views for managing instances. @@ -24,24 +25,42 @@ import logging from django import http from django import shortcuts +from django.views import generic from django.core.urlresolvers import reverse, reverse_lazy from django.utils.datastructures import SortedDict from django.utils.translation import ugettext_lazy as _ +from django.contrib.formtools.wizard.views import SessionWizardView from horizon import exceptions from horizon import forms from horizon import tabs from horizon import tables from horizon import workflows +from horizon.forms.views import ModalFormMixin from openstack_dashboard import api from .tables import WinDCTable, WinServicesTable from .workflows import CreateWinService, CreateWinDC +from .forms import WizardFormServiceType, WizardFormConfiguration - +import pdb LOG = logging.getLogger(__name__) +class Wizard(ModalFormMixin, SessionWizardView, generic.FormView): + template_name = 'project/windc/services_tabs.html' + + def done(self, form_list, **kwargs): + #do_something_with_the_form_data(form_list) + return HttpResponseRedirect('/') + + def get_form(self, step=None, data=None, files=None): + form = super(Wizard, self).get_form(step, data, files) + print step + print data + return form + + class IndexView(tables.DataTableView): table_class = WinDCTable template_name = 'project/windc/index.html' @@ -69,7 +88,7 @@ class WinServices(tables.DataTableView): def get_data(self): try: - dc_id = self.kwargs['domain_controller_id'] + dc_id = self.kwargs['data_center_id'] datacenter = api.windc.datacenters_get(self.request, dc_id) self.dc_name = datacenter.name services = api.windc.services_list(self.request, datacenter) @@ -91,6 +110,7 @@ class CreateWinDCView(workflows.WorkflowView): initial['user_id'] = self.request.user.id return initial + class CreateWinServiceView(workflows.WorkflowView): workflow_class = CreateWinService template_name = "project/windc/create.html" diff --git a/dashboard/windc/workflows.py b/dashboard/windc/workflows.py index 4eb32e9..dcd9b18 100644 --- a/dashboard/windc/workflows.py +++ b/dashboard/windc/workflows.py @@ -84,11 +84,7 @@ class ConfigureWinDCAction(workflows.Action): dc_name = forms.CharField(label=_("Domain Name"), required=False) - #dc_net_name = forms.CharField(label=_("Domain NetBIOS Name"), - # required=False, - # help_text=_("A NetBIOS name of new domain.")) - - dc_count = forms.IntegerField(label=_("Domain Controllers Count"), + dc_count = forms.IntegerField(label=_("Instances Count"), required=True, min_value=1, max_value=100, @@ -108,7 +104,7 @@ class ConfigureWinDCAction(workflows.Action): "Recovery Mode.")) class Meta: - name = _("Domain Controllers") + name = _("Active Directory") help_text_template = ("project/windc/_dc_help.html") @@ -169,7 +165,7 @@ class CreateWinService(workflows.Workflow): default_steps = (SelectProjectUser, ConfigureWinDC, ConfigureWinIIS) - + def format_status_message(self, message): dc_name = self.context.get('dc_name', 'noname') return message % dc_name @@ -192,7 +188,6 @@ class CreateWinService(workflows.Workflow): return False - class CreateWinDC(workflows.Workflow): slug = "create" name = _("Create Windows Data Center") diff --git a/dashboard/windcclient/v1/datacenters.py b/dashboard/windcclient/v1/datacenters.py index 636021d..3d184f1 100644 --- a/dashboard/windcclient/v1/datacenters.py +++ b/dashboard/windcclient/v1/datacenters.py @@ -35,9 +35,9 @@ class DCManager(base.Manager): body.update(extra) return self._create('/datacenters', body, 'datacenter') - def delete(self, datacenter): - return self._delete("/datacenters/%s" % base.getid(datacenter)) + def delete(self, datacenter_id): + return self._delete("/datacenters/%s" % datacenter_id) - def get(self, datacenter): - return self._get("/datacenters/%s" % base.getid(datacenter), + def get(self, datacenter_id): + return self._get("/datacenters/%s" % datacenter_id, 'datacenter') diff --git a/dashboard/windcclient/v1/services.py b/dashboard/windcclient/v1/services.py index 8309338..809a30c 100644 --- a/dashboard/windcclient/v1/services.py +++ b/dashboard/windcclient/v1/services.py @@ -37,10 +37,9 @@ class DCServiceManager(base.Manager): return self._create("/datacenters/%s/services" % base.getid(datacenter), body, 'service') - def delete(self, datacenter, service): + def delete(self, datacenter_id, service_id): return self._delete("/datacenters/%s/services/%s" % \ - (base.getid(datacenter), - base.getid(service))) + (datacenter_id, service_id)) def get(self, datacenter, service): return self._get("/datacenters/%s/services/%s" % \ diff --git a/tests/deploy.sh b/tests/deploy.sh new file mode 100644 index 0000000..e57207d --- /dev/null +++ b/tests/deploy.sh @@ -0,0 +1,82 @@ +#!/usr/bin/expect -d +# The following directories should be created for this script: +# /opt/stack/devstack +# /opt/stack/keero +# the ssh key should be in directory /opt/stack/.ssh/ +# the iso file with windows should be in directory /opt/stack/ + +set timeout 1200 + +send_user "\n\nStart to login to the test bed...\n\n" + +spawn /usr/bin/ssh [lindex $argv 0]@[lindex $argv 1] +expect "password" +send -- "EVYiMCVZX9\n" +expect "*#*" + +send -- "su - stack\n" +expect "*$*" + +send -- "sudo killall python\n" +expect "*$*" +send -- "cd ~/devstack\n" +expect "*$*" +send -- "./unstack.sh\n" +expect "*$*" +send -- "./stack.sh\n" +expect "*Would you like to start it now?*" +send -- "y\n" +expect "*stack.sh completed*" + +send -- "source openrc admin admin\n" +expect "*$*" + +send -- "cd ~\n" +expect "*$*" + +send -- "nova keypair-add keero-linux-keys > heat_key.priv\n" +expect "*$*" + +send -- "glance image-create --name 'ws-2012-full-agent' --is-public true --container-format ovf --disk-format qcow2 < ws-2012-full-agent.qcow2\n" +expect "*$*" + +send -- "cd ~/keero\n" +expect "*$*" +send -- "git pull\n" +expect "/.ssh/id_rsa" +send -- "swordfish\n" +expect "*$*" +send -- "cp -Rf ~/keero/dashboard/windc /opt/stack/horizon/openstack_dashboard/dashboards/project\n" +expect "*$*" +send -- "cp -f ~/keero/dashboard/api/windc.py /opt/stack/horizon/openstack_dashboard/api/\n" +expect "*$*" +send -- "cp -Rf ~/keero/dashboard/windcclient /opt/stack/horizon/\n" +expect "*$*" +send -- "cd ~/keero/windc\n" +expect "*$*" +send -- "rm -rf windc.sqlite\n" +expect "*$*" +send -- "./tools/with_venv.sh ./bin/windc-api --config-file=./etc/windc-api-paste.ini --dbsync\n" +expect "*$*" +send -- "logout\n" +expect "*#*" + +send -- "rabbitmq-plugins enable rabbitmq_management\n" +expect "*#*" +send -- "service rabbitmq-server restart\n" +expect "*#*" +send -- "rabbitmqctl add_user keero keero\n" +expect "*#*" +send -- "rabbitmqctl set_user_tags keero administrator\n" +expect "*#*" + +send -- "su - stack\n" +expect "*$*" +send -- "cd /opt/stack/devstack\n" +expect "*$*" +send -- "source openrc admin admin\n" +expect "*$*" +send -- "cd /opt/stack/keero/windc\n" +expect "*$*" +send -- "sudo ./tools/with_venv.sh ./bin/windc-api --config-file=./etc/windc-api-paste.ini > /opt/stack/tests_windc_daemon.log &\n" +expect "*$*" diff --git a/tests/selenium/conf.ini b/tests/selenium/conf.ini new file mode 100644 index 0000000..8b43288 --- /dev/null +++ b/tests/selenium/conf.ini @@ -0,0 +1,4 @@ +[server] +address=http://172.18.124.101 +user=admin +password=AkvareL707 \ No newline at end of file diff --git a/tests/selenium/datacenters_page.py b/tests/selenium/datacenters_page.py new file mode 100644 index 0000000..b354efa --- /dev/null +++ b/tests/selenium/datacenters_page.py @@ -0,0 +1,55 @@ +import re +from login_page import LoginPage + + +class DataCentersPage(): + page = None + + def __init__(self): + start_page = LoginPage() + self.page = start_page.login() + self.page.find_element_by_link_text('Project').click() + self.page.find_element_by_link_text('Windows Data Centers').click() + + def create_data_center(self, name): + button_text = 'Create Windows Data Center' + self.page.find_element_by_link_text(button_text).click() + + name_field = self.page.find_element_by_id('id_name') + xpath = "//input[@value='Create']" + button = self.page.find_element_by_xpath(xpath) + + name_field.clear() + name_field.send_keys(name) + + button.click() + + return self.page + + def find_data_center(self, name): + return self.page.find_element_by_link_text(name) + + def delete_data_center(self, name): + datacenter = self.find_data_center(name) + link = datacenter.get_attribute('href') + datacenter_id = re.search('windc/(\S+)', link).group(0)[6:-1] + + xpath = ".//*[@id='windc__row__%s']/td[3]/div/a[2]" % datacenter_id + more_button = self.page.find_element_by_xpath(xpath) + + more_button.click() + + delete_button_id = "windc__row_%s__action_delete" % datacenter_id + delete_button = self.page.find_element_by_id(delete_button_id) + + delete_button.click() + + self.page.find_element_by_link_text("Delete Data Center").click() + + return self.page + + def select_data_center(self, name): + datacenter = self.page.find_data_center(name) + datacenter.click() + + return self.page diff --git a/tests/selenium/login_page.py b/tests/selenium/login_page.py new file mode 100644 index 0000000..f0f357c --- /dev/null +++ b/tests/selenium/login_page.py @@ -0,0 +1,30 @@ +import ConfigParser +from selenium import webdriver + + +class LoginPage(): + + def login(self): + config = ConfigParser.RawConfigParser() + config.read('conf.ini') + url = config.get('server', 'address') + user = config.get('server', 'user') + password = config.get('server', 'password') + + page = webdriver.Firefox() + page.set_page_load_timeout(30) + page.implicitly_wait(30) + page.get(url) + name = page.find_element_by_name('username') + pwd = page.find_element_by_name('password') + xpath = "//button[@type='submit']" + button = page.find_element_by_xpath(xpath) + + name.clear() + name.send_keys(user) + pwd.clear() + pwd.send_keys(password) + + button.click() + + return page diff --git a/tests/selenium/services_page.py b/tests/selenium/services_page.py new file mode 100644 index 0000000..3571df0 --- /dev/null +++ b/tests/selenium/services_page.py @@ -0,0 +1,55 @@ +import ConfigParser +from selenium import webdriver + + +class ServicesPage(): + page = None + + def __init__(self, page): + self.page = page + + def create_service(self, service_type, parameters): + + button_id = 'services__action_CreateService' + button = self.page.find_element_by_id(button_id) + button.click() + + self.select_type_of_service(service_type) + + for parameter in parameters: + field = self.page.find_element_by_name(parameter.key) + field.clear() + field.send_keys(parameter.value) + + xpath = "//input[@value='Deploy']" + deploy_button = self.page.find_element_by_xpath(xpath) + deploy_button.click() + + return page + + def select_type_of_service(self, service_type): + tab = find_element_by_link_text(service_type) + tab.click() + return self.page + + def find_service(self, name): + return self.page.find_element_by_link_text(name) + + def delete_service(self, name): + service = self.find_data_center(name) + link = service.get_attribute('href') + + service_id = re.search('windc/(\S+)', link).group(0)[6:-1] + + xpath = ".//*[@id='services__row__%s']/td[5]/div/a[2]" % service_id + more_button = self.page.find_element_by_xpath(xpath) + more_button.click() + + delete_button_id = "services__row_%s__action_delete" % datacenter_id + delete_button = self.page.find_element_by_id(delete_button_id) + + delete_button.click() + + self.page.find_element_by_link_text("Delete Service").click() + + return self.page diff --git a/tests/selenium/test.py b/tests/selenium/test.py new file mode 100644 index 0000000..b95af41 --- /dev/null +++ b/tests/selenium/test.py @@ -0,0 +1,35 @@ +import untitests +from datacenters_page import DataCentersPage + + +class SanityTests(): + + def setUp(self): + self.page = DataCentersPage() + + def tearDown(self): + self.page.close() + + def test_01_create_data_center(self): + self.page.create_data_center('dc1') + assert self.page.find_data_center('dc1') is not None + + def test_02_delete_data_center(self): + page.delete_data_center('dc1') + assert self.page.find_data_center('dc1') is None + + def test_03_create_data_centers(self): + for i in range(1, 20): + name = 'datacenter' + str(i) + self.page.create_data_center(name) + assert self.page.find_data_center(name) is not None + + def test_04_delete_data_centers(self): + page.delete_data_center('datacenter1') + page.delete_data_center('datacenter20') + assert self.page.find_data_center('datacenter1') is None + assert self.page.find_data_center('datacenter20') is None + + for i in range(2, 19): + name = 'datacenter' + str(i) + assert self.page.find_data_center(name) is not None