diff --git a/devops-service/db/mongo/models/deploy_env_base.rb b/devops-service/db/mongo/models/deploy_env_base.rb index 2e9c35f..fa65a67 100644 --- a/devops-service/db/mongo/models/deploy_env_base.rb +++ b/devops-service/db/mongo/models/deploy_env_base.rb @@ -35,6 +35,10 @@ class DeployEnvBase < MongoModel } end + def provider_instance + @provider_instance ||= ::Provider::ProviderFactory.get(self.provider) + end + # class methods class << self diff --git a/devops-service/db/mongo/models/deploy_env_openstack.rb b/devops-service/db/mongo/models/deploy_env_openstack.rb index bab4c0b..6293c96 100644 --- a/devops-service/db/mongo/models/deploy_env_openstack.rb +++ b/devops-service/db/mongo/models/deploy_env_openstack.rb @@ -15,6 +15,14 @@ class DeployEnvOpenstack < DeployEnvBase :subnets => {:type => Array, :empty => true}, :groups => {:type => Array, :empty => false} + set_validators ::Validators::DeployEnv::RunList, + ::Validators::DeployEnv::Expiration, + ::Validators::DeployEnv::Users, + ::Validators::DeployEnv::Flavor, + ::Validators::DeployEnv::Image, + ::Validators::DeployEnv::SubnetNotEmpty, + ::Validators::DeployEnv::SubnetBelongsToProvider + def initialize d={} super(d) self.flavor = d["flavor"] @@ -28,9 +36,7 @@ class DeployEnvOpenstack < DeployEnvBase def validate! super - p = ::Provider::ProviderFactory.get(self.provider) - check_flavor!(p, self.flavor) - check_image!(p, self.image) + p = provider_instance check_subnets_and_groups!(p, self.subnets, self.groups) do |networks| if self.subnets.empty? raise InvalidRecord.new "Subnets array can not be empty" @@ -56,4 +62,10 @@ class DeployEnvOpenstack < DeployEnvBase DeployEnvOpenstack.new(hash) end + private + + def subnets_filter + nil + end + end diff --git a/devops-service/db/validators/base.rb b/devops-service/db/validators/base.rb index 74323a8..51c91c9 100644 --- a/devops-service/db/validators/base.rb +++ b/devops-service/db/validators/base.rb @@ -1,7 +1,8 @@ class Validators::Base - def initialize(model) + def initialize(model, options={}) @model = model + @options = options end def validate! diff --git a/devops-service/db/validators/deploy_env/flavor.rb b/devops-service/db/validators/deploy_env/flavor.rb new file mode 100644 index 0000000..0a0be25 --- /dev/null +++ b/devops-service/db/validators/deploy_env/flavor.rb @@ -0,0 +1,14 @@ +module Validators + class DeployEnv::Flavor < Base + + def valid? + @model.provider_instance.flavors.detect do |flavor| + flavor['id'] == @model.flavor + end + end + + def message + "Invalid flavor '#{@model.flavor}'." + end + end +end \ No newline at end of file diff --git a/devops-service/db/validators/deploy_env/groups.rb b/devops-service/db/validators/deploy_env/groups.rb new file mode 100644 index 0000000..4f64c6e --- /dev/null +++ b/devops-service/db/validators/deploy_env/groups.rb @@ -0,0 +1,14 @@ +module Validators + class DeployEnv::Groups < Base + + def valid? + subnets_filter = @model.send(:subnets_filter) + @invalid_groups = @model.groups - @model.provider_instance.groups(subnets_filter).keys + @invalid_groups.empty? + end + + def message + "Invalid groups '#{@invalid_groups.join("', '")}'" + end + end +end \ No newline at end of file diff --git a/devops-service/db/validators/deploy_env/image.rb b/devops-service/db/validators/deploy_env/image.rb new file mode 100644 index 0000000..45a2860 --- /dev/null +++ b/devops-service/db/validators/deploy_env/image.rb @@ -0,0 +1,18 @@ +require "commands/image" + +module Validators + class DeployEnv::Image < Base + include ::ImageCommands + + def valid? + images = get_images(DevopsService.mongo, @model.provider) + images.detect do |image| + image["id"] == @model.image + end + end + + def message + "Invalid image '#{@model.image}'." + end + end +end \ No newline at end of file diff --git a/devops-service/db/validators/deploy_env/subnet_belongs_to_provider.rb b/devops-service/db/validators/deploy_env/subnet_belongs_to_provider.rb new file mode 100644 index 0000000..9f37840 --- /dev/null +++ b/devops-service/db/validators/deploy_env/subnet_belongs_to_provider.rb @@ -0,0 +1,14 @@ +module Validators + class DeployEnv::SubnetBelongsToProvider < Base + + def valid? + provider_subnets = @model.provider_instance.networks.map { |n| n["name"] } + @invalid_subnets = @model.subnets - provider_subnets + @invalid_subnets.empty? + end + + def message + "Invalid subnets '#{@invalid_subnets.join("', '")}'." + end + end +end \ No newline at end of file diff --git a/devops-service/db/validators/deploy_env/subnet_not_empty.rb b/devops-service/db/validators/deploy_env/subnet_not_empty.rb new file mode 100644 index 0000000..22e079a --- /dev/null +++ b/devops-service/db/validators/deploy_env/subnet_not_empty.rb @@ -0,0 +1,12 @@ +module Validators + class DeployEnv::SubnetNotEmpty < Base + + def valid? + !@model.subnets.empty? + end + + def message + 'Subnets array can not be empty' + end + end +end \ No newline at end of file diff --git a/devops-service/providers/openstack_stub.rb b/devops-service/providers/openstack_stub.rb new file mode 100644 index 0000000..c5d3dab --- /dev/null +++ b/devops-service/providers/openstack_stub.rb @@ -0,0 +1,64 @@ +# Stub some methods in Openstack Provider + +puts '!!! WARNING !!!' +puts '!!! Some Openstack methods are stubbed !!!' + +class Provider::Openstack + + def groups filter=nil + { + 'test' => { + 'description' => 'Description', + 'rules' => [{ + "protocol" => "ip_protocol", + "from" => "from_port", + "to" => "to_port", + "cidr" => "cidr" + }] + }, + 'default' => { + 'description' => 'Description', + 'rules' => [{ + "protocol" => "ip_protocol", + "from" => "from_port", + "to" => "to_port", + "cidr" => "cidr" + }] + } + } + end + + def flavors + [{ + "id" => 'test_flavor', + "v_cpus" => 2, + "ram" => 256, + "disk" => 1000 + }] + end + + def images filters + [ + { + "id" => 'test_image', + "name" => 'test image', + "status" => 'test status' + } + ] + end + + def networks + networks_detail + end + + def networks_detail + [ + { + 'cidr' => '192.0.2.32/27', + 'name' => 'test_network', + 'id' => 'test_network_id' + } + ] + end + +end \ No newline at end of file diff --git a/devops-service/providers/provider_factory.rb b/devops-service/providers/provider_factory.rb index 59bff68..af5afe5 100644 --- a/devops-service/providers/provider_factory.rb +++ b/devops-service/providers/provider_factory.rb @@ -23,6 +23,11 @@ module Provider ["ec2", "openstack", "static"].each do |p| begin require "providers/#{p}" + + if File.exist?("providers/#{p}_stub.rb") + require "providers/#{p}_stub" + end + o = Provider.const_get(p.capitalize).new(conf) if o.configured? @@providers[p] = o diff --git a/devops-service/tests/features/step_definitions/http_queries_steps.rb b/devops-service/tests/features/step_definitions/http_queries_steps.rb index b34e5b6..14321ae 100644 --- a/devops-service/tests/features/step_definitions/http_queries_steps.rb +++ b/devops-service/tests/features/step_definitions/http_queries_steps.rb @@ -100,6 +100,13 @@ Then(/^response should be '(\d+)'$/) do |code| assert(code.to_i == last_response.status, "Status is not #{code}, it is #{last_response.status}") end +Then(/^response error should be "([^"]+)"$/) do |error_msg| + json = JSON.parse(last_response.body) + message = json['message'] + stripped_message = message.sub(/Project '\w+'. Deploy environment '\w+'. /, '') + assert(error_msg == stripped_message, "Error message is not \n #{error_msg}\nit is \n #{stripped_message}") +end + Then(/^the Content\-Type header should include 'application\/json'$/) do assert last_response.header.contenttype.include?("application/json"), "Response has no header 'Content-Type' with 'application/json'" end diff --git a/devops-service/tests/generate_tests.rb b/devops-service/tests/generate_tests.rb index dd7cdc2..478ffb2 100755 --- a/devops-service/tests/generate_tests.rb +++ b/devops-service/tests/generate_tests.rb @@ -4,6 +4,7 @@ require "erb" require "yaml" require "ostruct" require "fileutils" +require "./templates/fixtures/fixture_formatter" class Generator < OpenStruct @@ -12,6 +13,7 @@ class Generator < OpenStruct def initialize @config = YAML.load_file(File.new(ENV["CONFIG"] || CONFIG)) + load_fixtures() super(:config => @config) end @@ -52,6 +54,12 @@ class Generator < OpenStruct def render(template) ERB.new(template).result(binding) end + + def load_fixtures + @fixtures = {} + @fixtures['deploy_env'] = YAML.load_file('templates/fixtures/deploy_env.yml') + @formatter = FixtureFormatter.new(@fixtures) + end end templates = { diff --git a/devops-service/tests/templates/api_v2/10_create/40_deploy_env.feature.erb b/devops-service/tests/templates/api_v2/10_create/40_deploy_env.feature.erb index 728b6a4..646e9ad 100644 --- a/devops-service/tests/templates/api_v2/10_create/40_deploy_env.feature.erb +++ b/devops-service/tests/templates/api_v2/10_create/40_deploy_env.feature.erb @@ -1,24 +1,217 @@ @project @deploy_env Feature: Create deploy env - <% project_name = @config["static"]["project"]["name"] %> - @static - Scenario: Send deploy env with invalid runlist for project <%= project_name %> - When I send PUT '/v2.0/project/<%= project_name %>' query with JSON body + #--------------------- + # OPENSTACK + #--------------------- + + <% openstack_project_name = @config["openstack"]["project"]["name"] %> + <% precreated_openstack_deploy_env = '{ "identifier": "test", "run_list": [], "expires": null, "provider": "openstack", "users": [ "test" ], "flavor": "test_flavor", "image": "test_image", "subnets": ["test_network"] }' %> + @openstack + Scenario: Send deploy env with invalid runlist for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body """ { "deploy_envs": [ - // precreated deploy env + <%= @formatter.json('deploy_env/openstack/valid', spaces: 8) %>, { - "identifier": "test", + "identifier": "test2", + "run_list": [ "wrong_role"], + "expires": null, + "provider": "openstack", + "users": [ "test" ], + "flavor": "test_flavor", + "image": "test_image", + "subnets": ["test_network"] + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '400' + And response error should be "Invalid run list elements: 'wrong_role'. Each element should be role or recipe." + + @openstack + Scenario: Send deploy env with invalid expiration for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_openstack_deploy_env %>, + { + "identifier": "test2", + "run_list": [], + "expires": "10_wrong", + "provider": "openstack", + "users": [ "test" ], + "flavor": "test_flavor", + "image": "test_image", + "subnets": ["test_network"] + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '400' + And response error should be "Parameter 'expires' is invalid. Valid format: [0-9]+[smhdw] or null." + + @openstack + Scenario: Send deploy env with invalid user (missing in mongo) for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_openstack_deploy_env %>, + { + "identifier": "test2", "run_list": [], "expires": null, - "provider": "static", - "users": [ - "test" - ] - }, - // new deploy env + "provider": "openstack", + "users": [ "non_existing_user" ], + "flavor": "test_flavor", + "image": "test_image", + "subnets": ["test_network"] + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '400' + And response error should be "These users are missing in mongo: 'non_existing_user'." + + @openstack + Scenario: Send deploy env with invalid flavor for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_openstack_deploy_env %>, + { + "identifier": "test2", + "run_list": [ "role[test2_t2]" ], + "expires": null, + "provider": "openstack", + "users": [ "test" ], + "flavor": "wrong_flavor", + "image": "test_image", + "subnets": ["test_network"] + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '400' + And response error should be "Invalid flavor 'wrong_flavor'." + + @openstack + Scenario: Send deploy env with invalid image for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_openstack_deploy_env %>, + { + "identifier": "test2", + "run_list": [ "role[test2_t2]" ], + "expires": null, + "provider": "openstack", + "users": [ "test" ], + "flavor": "test_flavor", + "image": "wrong_image", + "subnets": ["test_network"] + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '400' + And response error should be "Invalid image 'wrong_image'." + + @openstack + Scenario: Send deploy env with empty subnets for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_openstack_deploy_env %>, + { + "identifier": "test2", + "run_list": [ "role[test2_t2]" ], + "expires": null, + "provider": "openstack", + "users": [ "test" ], + "flavor": "test_flavor", + "image": "test_image", + "subnets": null + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '400' + And response error should be "Subnets array can not be empty" + + @openstack + Scenario: Send deploy env with invalid subnets for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_openstack_deploy_env %>, + { + "identifier": "test2", + "run_list": [ "role[test2_t2]" ], + "expires": null, + "provider": "openstack", + "users": [ "test" ], + "flavor": "test_flavor", + "image": "test_image", + "subnets": ["wrong_subnet"] + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '400' + And response error should be "Invalid subnets 'wrong_subnet'." + + + @openstack + Scenario: Create deploy env for project <%= openstack_project_name %> + When I send PUT '/v2.0/project/<%= openstack_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_openstack_deploy_env %>, + { + "identifier": "test2", + "run_list": [], + "expires": null, + "provider": "openstack", + "users": [ "test" ], + "flavor": "test_flavor", + "image": "test_image", + "subnets": ["test_network"] + } + ], + "name": "<%= openstack_project_name %>" + } + """ + Then response should be '200' + + #--------------------- + # STATIC + #--------------------- + + <% static_project_name = @config["static"]["project"]["name"] %> + <% precreated_static_deploy_env = '{ "identifier": "test", "run_list": [], "expires": null, "provider": "static", "users": [ "test" ] }' %> + @static + Scenario: Send deploy env with invalid runlist for project <%= static_project_name %> + When I send PUT '/v2.0/project/<%= static_project_name %>' query with JSON body + """ + { + "deploy_envs": [ + <%= precreated_static_deploy_env %>, { "identifier": "test2", "run_list": [ @@ -26,141 +219,71 @@ Feature: Create deploy env ], "expires": null, "provider": "static", - "users": [ - "test" - ] + "users": [ "test" ] } ], - "name": "<%= project_name %>" + "name": "<%= static_project_name %>" } """ Then response should be '400' @static - Scenario: Send deploy env with invalid expiration for project <%= project_name %> - When I send PUT '/v2.0/project/<%= project_name %>' query with JSON body + Scenario: Send deploy env with invalid expiration for project <%= static_project_name %> + When I send PUT '/v2.0/project/<%= static_project_name %>' query with JSON body """ { "deploy_envs": [ - // precreated deploy env - { - "identifier": "test", - "run_list": [], - "expires": null, - "provider": "static", - "users": [ - "test" - ] - }, - // new deploy env + <%= precreated_static_deploy_env %>, { "identifier": "test2", "run_list": [], "expires": "10asd", "provider": "static", - "users": [ - "test" - ] + "users": [ "test" ] } ], - "name": "<%= project_name %>" + "name": "<%= static_project_name %>" } """ Then response should be '400' @static - Scenario: Send deploy env with invalid expiration for project <%= project_name %> - When I send PUT '/v2.0/project/<%= project_name %>' query with JSON body + Scenario: Send deploy env with invalid user (missing in mongo) for project <%= static_project_name %> + When I send PUT '/v2.0/project/<%= static_project_name %>' query with JSON body """ { "deploy_envs": [ - // precreated deploy env - { - "identifier": "test", - "run_list": [], - "expires": null, - "provider": "static", - "users": [ - "test" - ] - }, - // new deploy env - { - "identifier": "test2", - "run_list": [], - "expires": "10asd", - "provider": "static", - "users": [ - "test" - ] - } - ], - "name": "<%= project_name %>" - } - """ - Then response should be '400' - - @static - Scenario: Send deploy env with invalid user (missing in mongo) for project <%= project_name %> - When I send PUT '/v2.0/project/<%= project_name %>' query with JSON body - """ - { - "deploy_envs": [ - // precreated deploy env - { - "identifier": "test", - "run_list": [], - "expires": null, - "provider": "static", - "users": [ - "test" - ] - }, - // new deploy env + <%= precreated_static_deploy_env %>, { "identifier": "test2", "run_list": [], "expires": null, "provider": "static", - "users": [ - "non_existing_user" - ] + "users": [ "non_existing_user" ] } ], - "name": "<%= project_name %>" + "name": "<%= static_project_name %>" } """ Then response should be '400' @static - Scenario: Create deploy env for project <%= project_name %> - When I send PUT '/v2.0/project/<%= project_name %>' query with JSON body + Scenario: Create deploy env for project <%= static_project_name %> + When I send PUT '/v2.0/project/<%= static_project_name %>' query with JSON body """ { "deploy_envs": [ - // precreated deploy env - { - "identifier": "test", - "run_list": [], - "expires": null, - "provider": "static", - "users": [ - "test" - ] - }, - // new deploy env + <%= precreated_static_deploy_env %>, { "identifier": "test2", "run_list": [], "expires": null, "provider": "static", - "users": [ - "test" - ] + "users": [ "test" ] } ], - "name": "<%= project_name %>" + "name": "<%= static_project_name %>" } """ Then response should be '200' diff --git a/devops-service/tests/templates/api_v2/90_delete/20_deploy_env.feature.erb b/devops-service/tests/templates/api_v2/90_delete/20_deploy_env.feature.erb index 2b52d8a..f4e9740 100644 --- a/devops-service/tests/templates/api_v2/90_delete/20_deploy_env.feature.erb +++ b/devops-service/tests/templates/api_v2/90_delete/20_deploy_env.feature.erb @@ -8,6 +8,7 @@ Feature: Delete deploy env """ { "deploy_envs": [ + // Remove created deploy_envs, leave only the first one (precreated). { "identifier": "test", "run_list": [], diff --git a/devops-service/tests/templates/fixtures/deploy_env.yml b/devops-service/tests/templates/fixtures/deploy_env.yml new file mode 100644 index 0000000..b06ef72 --- /dev/null +++ b/devops-service/tests/templates/fixtures/deploy_env.yml @@ -0,0 +1,14 @@ +openstack: + valid: &valid + "identifier": "test" + "run_list": [] + "expires": "10d" + "provider": "openstack" + "users": [ "test" ] + "flavor": "test_flavor" + "image": "test_image" + "subnets": ["test_network"] + invalid: + run_list: + <<: *valid + "run_list": [ "wrong_role"] \ No newline at end of file diff --git a/devops-service/tests/templates/fixtures/fixture_formatter.rb b/devops-service/tests/templates/fixtures/fixture_formatter.rb new file mode 100644 index 0000000..0fa3834 --- /dev/null +++ b/devops-service/tests/templates/fixtures/fixture_formatter.rb @@ -0,0 +1,41 @@ +require 'json' + +class FixtureFormatter + + def initialize(fixtures) + @fixtures = fixtures + end + + def json(path, options={}) + result = JSON.pretty_generate(get_fixture(path)) + if options[:spaces] + result = shift_to_right(result, options[:spaces]) + end + result + end + + private + + def get_fixture(path) + keys = path.split('/') + hash = @fixtures + keys.each do |key| + hash = hash[key] + end + hash + end + + def shift_to_right(text, spaces_count) + buffer = '' + first_line = true + text.each_line do |line| + if first_line + first_line = false + buffer += line + next + end + buffer += (' ' * spaces_count) + line + end + buffer + end +end \ No newline at end of file