blob: 5038261be26ab4cbbb8d931c8fce03aaf4712e37 [file] [log] [blame]
# Copyright 2019 Google LLC
#
# 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.
"""A module to provide test plan APIs."""
# Non-standard docstrings are used to generate the API documentation.
import croniter
import endpoints
from protorpc import message_types
from protorpc import messages
from protorpc import remote
from tradefed_cluster.util import ndb_shim as ndb
from multitest_transport.api import base
from multitest_transport.models import build
from multitest_transport.models import messages as mtt_messages
from multitest_transport.models import ndb_models
from multitest_transport.test_scheduler import test_kicker
from multitest_transport.test_scheduler import test_plan_kicker
from multitest_transport.test_scheduler import test_scheduler
def _IsValidCronExpression(cron_exp):
"""Check validity of a given cron expression.
Args:
cron_exp: a cron expression.
Returns:
True if valid. Otherwise False.
"""
try:
croniter.croniter(cron_exp)
except ValueError:
return False
return True
def _ValidateTestPlan(test_plan):
"""Check validity of a given test plan.
Args:
test_plan: a ndb_models.TestPlan object.
"""
if test_plan.cron_exp and not _IsValidCronExpression(test_plan.cron_exp):
raise endpoints.BadRequestException(
('Invalid cron expression (%s). Cron expression should be ordered as '
'minute, hour, day of month, month, day of week.')
% test_plan.cron_exp)
for sequence in test_plan.test_run_sequences:
for config in sequence.test_run_configs:
if not config.test_key.get():
raise endpoints.BadRequestException(
'Test %s does not exist' % config.test_key.id())
config_device_actions = ndb.get_multi(
config.before_device_action_keys)
if not all(config_device_actions):
raise endpoints.BadRequestException(
'Cannot find some device actions: %s -> %s' % (
config.before_device_action_keys, config_device_actions))
test_kicker.ValidateDeviceActions(config_device_actions)
for obj in config.test_resource_objs:
build_locator = build.BuildLocator.ParseUrl(obj.url)
if build_locator and not mtt_messages.ConvertToKey(
ndb_models.BuildChannelConfig,
build_locator.build_channel_id).get():
raise endpoints.BadRequestException(
'Build channel %s does not exist' %
build_locator.build_channel_id)
@base.MTT_API.api_class(resource_name='test_plan', path='test_plans')
class TestPlanApi(remote.Service):
"""A handler for Test Plan API."""
@base.ApiMethod(
endpoints.ResourceContainer(message_types.VoidMessage),
mtt_messages.TestPlanList, path='/test_plans', http_method='GET',
name='list')
def List(self, request):
"""Lists test plans."""
test_plans = list(ndb_models.TestPlan.query()
.order(ndb_models.TestPlan.name))
test_plan_msgs = mtt_messages.ConvertList(
test_plans, mtt_messages.TestPlan)
return mtt_messages.TestPlanList(test_plans=test_plan_msgs)
@base.ApiMethod(
endpoints.ResourceContainer(mtt_messages.TestPlan),
mtt_messages.TestPlan, path='/test_plans', http_method='POST',
name='create')
def Create(self, request):
"""Creates a test plan.
Body:
Test plan data
"""
test_plan = mtt_messages.Convert(
request, ndb_models.TestPlan, from_cls=mtt_messages.TestPlan)
_ValidateTestPlan(test_plan)
test_plan.key = None
test_plan.put()
test_scheduler.ScheduleTestPlanCronJob(test_plan.key.id())
return mtt_messages.Convert(test_plan, mtt_messages.TestPlan)
@base.ApiMethod(
endpoints.ResourceContainer(
message_types.VoidMessage,
test_plan_id=messages.StringField(1, required=True)),
mtt_messages.TestPlan, path='{test_plan_id}', http_method='GET',
name='get')
def Get(self, request):
"""Fetches a test plan.
Parameters:
test_plan_id: Test plan ID, or zero for an empty test plan
"""
if request.test_plan_id == '0':
# For ID 0, return an empty test plan object to use as a template in UI.
test_plan = ndb_models.TestPlan(id=0, name='')
else:
test_plan = mtt_messages.ConvertToKey(
ndb_models.TestPlan, request.test_plan_id).get()
if not test_plan:
raise endpoints.NotFoundException(
'No test plan with ID %s' % request.test_plan_id)
return mtt_messages.Convert(test_plan, mtt_messages.TestPlan)
@base.ApiMethod(
endpoints.ResourceContainer(
mtt_messages.TestPlan,
test_plan_id=messages.StringField(1, required=True)),
mtt_messages.TestPlan, path='{test_plan_id}', http_method='PUT',
name='update')
def Update(self, request):
"""Updates a test plan.
Body:
Test plan data
Parameters:
test_plan_id: Test plan ID
"""
test_plan_key = mtt_messages.ConvertToKey(
ndb_models.TestPlan, request.test_plan_id)
if not test_plan_key.get():
raise endpoints.NotFoundException()
test_plan = mtt_messages.Convert(
request, ndb_models.TestPlan, from_cls=mtt_messages.TestPlan)
_ValidateTestPlan(test_plan)
test_plan.key = test_plan_key
test_plan.put()
test_scheduler.ScheduleTestPlanCronJob(test_plan.key.id())
return mtt_messages.Convert(test_plan, mtt_messages.TestPlan)
@base.ApiMethod(
endpoints.ResourceContainer(
message_types.VoidMessage,
test_plan_id=messages.StringField(1, required=True)),
message_types.VoidMessage, path='{test_plan_id}',
http_method='DELETE', name='delete')
def Delete(self, request):
"""Deletes a test plan.
Parameters:
test_plan_id: Test plan ID
"""
test_plan_key = mtt_messages.ConvertToKey(
ndb_models.TestPlan, request.test_plan_id)
test_plan_key.delete()
return message_types.VoidMessage()
@base.ApiMethod(
endpoints.ResourceContainer(
message_types.VoidMessage,
test_plan_id=messages.StringField(1, required=True)),
message_types.VoidMessage, path='{test_plan_id}/run', http_method='POST',
name='run')
def Run(self, request):
"""Runs a test plan immediately.
Parameters:
test_plan_id: Test plan ID
"""
test_plan = mtt_messages.ConvertToKey(
ndb_models.TestPlan, request.test_plan_id).get()
if not test_plan:
raise endpoints.NotFoundException('Test plan %s does not exist'
% request.test_plan_id)
if not test_plan.test_run_sequences:
raise endpoints.BadRequestException(
'No test run configs with ID %s' % request.test_plan_id)
test_plan_kicker.KickTestPlan(test_plan.key.id())
# TODO: returns a test plan job object.
return message_types.VoidMessage()