diff --git a/.travis.yml b/.travis.yml index 29b1d31a3..a75924ff4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,7 +59,7 @@ script: - sudo mkdir -p /var/lib/ciao/instances - sudo chmod 0777 /var/lib/ciao/instances - test-cases -v -timeout 9 -coverprofile /tmp/cover.out -short github.com/01org/ciao/ciao-controller/... - - test-cases -v -timeout 9 -coverprofile /tmp/cover.out -append-profile -short github.com/01org/ciao/ciao-launcher github.com/01org/ciao/ciao-scheduler github.com/01org/ciao/payloads github.com/01org/ciao/configuration github.com/01org/ciao/testutil github.com/01org/ciao/ssntp/uuid github.com/01org/ciao/qemu github.com/01org/ciao/openstack/... + - test-cases -v -timeout 9 -coverprofile /tmp/cover.out -append-profile -short github.com/01org/ciao/ciao-launcher github.com/01org/ciao/ciao-scheduler github.com/01org/ciao/payloads github.com/01org/ciao/configuration github.com/01org/ciao/testutil github.com/01org/ciao/ssntp/uuid github.com/01org/ciao/qemu github.com/01org/ciao/openstack/... github.com/01org/ciao/bat - export GOROOT=`go env GOROOT` && sudo -E PATH=$PATH:$GOROOT/bin $GOPATH/bin/test-cases -v -timeout 9 -coverprofile /tmp/cover.out -append-profile github.com/01org/ciao/ssntp - export GOROOT=`go env GOROOT` && export SNNET_ENV=198.51.100.0/24 && sudo -E PATH=$PATH:$GOROOT/bin $GOPATH/bin/test-cases -v -timeout 9 -short -tags travis -coverprofile /tmp/cover.out -append-profile github.com/01org/ciao/networking/libsnnet # API Documentation is automated by swagger, every PR will verify the documentation can be generated diff --git a/_release/bat/README b/_release/bat/README deleted file mode 100644 index 1c032f804..000000000 --- a/_release/bat/README +++ /dev/null @@ -1,525 +0,0 @@ -Help on module release_test: - -NAME - release_test - Basic Acceptance Tests for the ciao project - -FILE - _release/bat/release_test.py - -DESCRIPTION - This module contains a set of tests that should be run on a ciao - cluster prior to submitting a pull request. The tests can be - run on a physical cluster, or on the ciao single VM setup. - The output is a TAP (Test Anything Protocol) format file, report.tap. - These tests utilize the python unittest framework. - - Prior to running the tests, the following environment variables - must be set: - "CIAO_IDENTITY" - the URL and port number of your identity service - "CIAO_CONTROLLER" - the URL and port number of the ciao compute service - "CIAO_USERNAME" - a test user with user level access to a test tenant - "CIAO_PASSWORD" - your test user's password - "CIAO_ADMIN_USERNAME" - your cluster admin user name - "CIAO_ADMIN_PASSWORD" - your cluster admin pass word. - - There are 2 configurable parameters that may be set: - - command_timeout - the length of time the test will wait for a - ciao-cli command to return. (default is 30 seconds) - cluster_timeout - the length of time to wait till an action has occurred - in the cluster (default is 60 seconds). - -CLASSES - unittest.case.TestCase(__builtin__.object) - BATTests - - class BATTests(unittest.case.TestCase) - | Basic Acceptance Tests for the ciao project - | - | Method resolution order: - | BATTests - | unittest.case.TestCase - | __builtin__.object - | - | Methods defined here: - | - | setUp(self) - | - | tearDown(self) - | - | test_cluster_status(self) - | Confirm that the cluster is ready - | - | test_delete_all_instances(self) - | Start a random workload, then delete it - | - | test_get_cncis(self) - | Start a random workload, then get CNCI information - | - | test_get_instances(self) - | Start a random workload, then make sure it's listed - | - | test_get_tenants(self) - | Get all tenants - | - | test_get_workloads(self) - | Get all available workloads - | - | test_start_all_workloads(self) - | Start one instance of all workloads - | - | test_start_all_workloads10(self) - | Start 10 instances of all workloads - | - | ---------------------------------------------------------------------- - | Methods inherited from unittest.case.TestCase: - | - | __call__(self, *args, **kwds) - | - | __eq__(self, other) - | - | __hash__(self) - | - | __init__(self, methodName='runTest') - | Create an instance of the class that will use the named test - | method when executed. Raises a ValueError if the instance does - | not have a method with the specified name. - | - | __ne__(self, other) - | - | __repr__(self) - | - | __str__(self) - | - | addCleanup(self, function, *args, **kwargs) - | Add a function, with arguments, to be called when the test is - | completed. Functions added are called on a LIFO basis and are - | called after tearDown on test failure or success. - | - | Cleanup items are called even if setUp fails (unlike tearDown). - | - | addTypeEqualityFunc(self, typeobj, function) - | Add a type specific assertEqual style function to compare a type. - | - | This method is for use by TestCase subclasses that need to register - | their own type equality functions to provide nicer error messages. - | - | Args: - | typeobj: The data type to call this function on when both values - | are of the same type in assertEqual(). - | function: The callable taking two arguments and an optional - | msg= argument that raises self.failureException with a - | useful error message when the two arguments are not equal. - | - | assertAlmostEqual(self, first, second, places=None, msg=None, delta=None) - | Fail if the two objects are unequal as determined by their - | difference rounded to the given number of decimal places - | (default 7) and comparing to zero, or by comparing that the - | between the two objects is more than the given delta. - | - | Note that decimal places (from zero) are usually not the same - | as significant digits (measured from the most signficant digit). - | - | If the two objects compare equal then they will automatically - | compare almost equal. - | - | assertAlmostEquals = assertAlmostEqual(self, first, second, places=None, msg=None, delta=None) - | Fail if the two objects are unequal as determined by their - | difference rounded to the given number of decimal places - | (default 7) and comparing to zero, or by comparing that the - | between the two objects is more than the given delta. - | - | Note that decimal places (from zero) are usually not the same - | as significant digits (measured from the most signficant digit). - | - | If the two objects compare equal then they will automatically - | compare almost equal. - | - | assertDictContainsSubset(self, expected, actual, msg=None) - | Checks whether actual is a superset of expected. - | - | assertDictEqual(self, d1, d2, msg=None) - | - | assertEqual(self, first, second, msg=None) - | Fail if the two objects are unequal as determined by the '==' - | operator. - | - | assertEquals = assertEqual(self, first, second, msg=None) - | Fail if the two objects are unequal as determined by the '==' - | operator. - | - | assertFalse(self, expr, msg=None) - | Check that the expression is false. - | - | assertGreater(self, a, b, msg=None) - | Just like self.assertTrue(a > b), but with a nicer default message. - | - | assertGreaterEqual(self, a, b, msg=None) - | Just like self.assertTrue(a >= b), but with a nicer default message. - | - | assertIn(self, member, container, msg=None) - | Just like self.assertTrue(a in b), but with a nicer default message. - | - | assertIs(self, expr1, expr2, msg=None) - | Just like self.assertTrue(a is b), but with a nicer default message. - | - | assertIsInstance(self, obj, cls, msg=None) - | Same as self.assertTrue(isinstance(obj, cls)), with a nicer - | default message. - | - | assertIsNone(self, obj, msg=None) - | Same as self.assertTrue(obj is None), with a nicer default message. - | - | assertIsNot(self, expr1, expr2, msg=None) - | Just like self.assertTrue(a is not b), but with a nicer default message. - | - | assertIsNotNone(self, obj, msg=None) - | Included for symmetry with assertIsNone. - | - | assertItemsEqual(self, expected_seq, actual_seq, msg=None) - | An unordered sequence specific comparison. It asserts that - | actual_seq and expected_seq have the same element counts. - | Equivalent to:: - | - | self.assertEqual(Counter(iter(actual_seq)), - | Counter(iter(expected_seq))) - | - | Asserts that each element has the same count in both sequences. - | Example: - | - [0, 1, 1] and [1, 0, 1] compare equal. - | - [0, 0, 1] and [0, 1] compare unequal. - | - | assertLess(self, a, b, msg=None) - | Just like self.assertTrue(a < b), but with a nicer default message. - | - | assertLessEqual(self, a, b, msg=None) - | Just like self.assertTrue(a <= b), but with a nicer default message. - | - | assertListEqual(self, list1, list2, msg=None) - | A list-specific equality assertion. - | - | Args: - | list1: The first list to compare. - | list2: The second list to compare. - | msg: Optional message to use on failure instead of a list of - | differences. - | - | assertMultiLineEqual(self, first, second, msg=None) - | Assert that two multi-line strings are equal. - | - | assertNotAlmostEqual(self, first, second, places=None, msg=None, delta=None) - | Fail if the two objects are equal as determined by their - | difference rounded to the given number of decimal places - | (default 7) and comparing to zero, or by comparing that the - | between the two objects is less than the given delta. - | - | Note that decimal places (from zero) are usually not the same - | as significant digits (measured from the most signficant digit). - | - | Objects that are equal automatically fail. - | - | assertNotAlmostEquals = assertNotAlmostEqual(self, first, second, places=None, msg=None, delta=None) - | Fail if the two objects are equal as determined by their - | difference rounded to the given number of decimal places - | (default 7) and comparing to zero, or by comparing that the - | between the two objects is less than the given delta. - | - | Note that decimal places (from zero) are usually not the same - | as significant digits (measured from the most signficant digit). - | - | Objects that are equal automatically fail. - | - | assertNotEqual(self, first, second, msg=None) - | Fail if the two objects are equal as determined by the '!=' - | operator. - | - | assertNotEquals = assertNotEqual(self, first, second, msg=None) - | Fail if the two objects are equal as determined by the '!=' - | operator. - | - | assertNotIn(self, member, container, msg=None) - | Just like self.assertTrue(a not in b), but with a nicer default message. - | - | assertNotIsInstance(self, obj, cls, msg=None) - | Included for symmetry with assertIsInstance. - | - | assertNotRegexpMatches(self, text, unexpected_regexp, msg=None) - | Fail the test if the text matches the regular expression. - | - | assertRaises(self, excClass, callableObj=None, *args, **kwargs) - | Fail unless an exception of class excClass is raised - | by callableObj when invoked with arguments args and keyword - | arguments kwargs. If a different type of exception is - | raised, it will not be caught, and the test case will be - | deemed to have suffered an error, exactly as for an - | unexpected exception. - | - | If called with callableObj omitted or None, will return a - | context object used like this:: - | - | with self.assertRaises(SomeException): - | do_something() - | - | The context manager keeps a reference to the exception as - | the 'exception' attribute. This allows you to inspect the - | exception after the assertion:: - | - | with self.assertRaises(SomeException) as cm: - | do_something() - | the_exception = cm.exception - | self.assertEqual(the_exception.error_code, 3) - | - | assertRaisesRegexp(self, expected_exception, expected_regexp, callable_obj=None, *args, **kwargs) - | Asserts that the message in a raised exception matches a regexp. - | - | Args: - | expected_exception: Exception class expected to be raised. - | expected_regexp: Regexp (re pattern object or string) expected - | to be found in error message. - | callable_obj: Function to be called. - | args: Extra args. - | kwargs: Extra kwargs. - | - | assertRegexpMatches(self, text, expected_regexp, msg=None) - | Fail the test unless the text matches the regular expression. - | - | assertSequenceEqual(self, seq1, seq2, msg=None, seq_type=None) - | An equality assertion for ordered sequences (like lists and tuples). - | - | For the purposes of this function, a valid ordered sequence type is one - | which can be indexed, has a length, and has an equality operator. - | - | Args: - | seq1: The first sequence to compare. - | seq2: The second sequence to compare. - | seq_type: The expected datatype of the sequences, or None if no - | datatype should be enforced. - | msg: Optional message to use on failure instead of a list of - | differences. - | - | assertSetEqual(self, set1, set2, msg=None) - | A set-specific equality assertion. - | - | Args: - | set1: The first set to compare. - | set2: The second set to compare. - | msg: Optional message to use on failure instead of a list of - | differences. - | - | assertSetEqual uses ducktyping to support different types of sets, and - | is optimized for sets specifically (parameters must support a - | difference method). - | - | assertTrue(self, expr, msg=None) - | Check that the expression is true. - | - | assertTupleEqual(self, tuple1, tuple2, msg=None) - | A tuple-specific equality assertion. - | - | Args: - | tuple1: The first tuple to compare. - | tuple2: The second tuple to compare. - | msg: Optional message to use on failure instead of a list of - | differences. - | - | assert_ = assertTrue(self, expr, msg=None) - | Check that the expression is true. - | - | countTestCases(self) - | - | debug(self) - | Run the test without collecting errors in a TestResult - | - | defaultTestResult(self) - | - | doCleanups(self) - | Execute all cleanup functions. Normally called for you after - | tearDown. - | - | fail(self, msg=None) - | Fail immediately, with the given message. - | - | failIf = deprecated_func(*args, **kwargs) - | - | failIfAlmostEqual = deprecated_func(*args, **kwargs) - | - | failIfEqual = deprecated_func(*args, **kwargs) - | - | failUnless = deprecated_func(*args, **kwargs) - | - | failUnlessAlmostEqual = deprecated_func(*args, **kwargs) - | - | failUnlessEqual = deprecated_func(*args, **kwargs) - | - | failUnlessRaises = deprecated_func(*args, **kwargs) - | - | id(self) - | - | run(self, result=None) - | - | shortDescription(self) - | Returns a one-line description of the test, or None if no - | description has been provided. - | - | The default implementation of this method returns the first line of - | the specified test method's docstring. - | - | skipTest(self, reason) - | Skip this test. - | - | ---------------------------------------------------------------------- - | Class methods inherited from unittest.case.TestCase: - | - | setUpClass(cls) from __builtin__.type - | Hook method for setting up class fixture before running tests in the class. - | - | tearDownClass(cls) from __builtin__.type - | Hook method for deconstructing the class fixture after running all tests in the class. - | - | ---------------------------------------------------------------------- - | Data descriptors inherited from unittest.case.TestCase: - | - | __dict__ - | dictionary for instance variables (if defined) - | - | __weakref__ - | list of weak references to the object (if defined) - | - | ---------------------------------------------------------------------- - | Data and other attributes inherited from unittest.case.TestCase: - | - | failureException = - | Assertion failed. - | - | longMessage = False - | - | maxDiff = 640 - -FUNCTIONS - check_cluster_status() - Confirms that the ciao cluster is fully operational - - This function uses ciao-cli to get the list of all compute/network nodes. - It confirms that the number of ready nodes is equal to the total number of nodes - It is called with the admin context. - - Returns: - A boolean indicating whether the cluster is ready or not. - - ciao_admin_env() - Sets the user environment up for ciao-cli with the admin role - - Copies the current environment and returns an environment - that ciao-cli will use for admin role operations - - Returns: - an os env dict - - ciao_user_env() - Sets the user environment up for ciao-cli with the user role - - Copies the current environment and returns an environment - that ciao-cli will use for user role operations - - Returns: - an os env dict - - delete_all_instances() - Deletes all instances for a particular tenant - - This function uses ciao-cli to try to delete all previously created instances. - It then confirms that the instances were deleted by looping for retry_count - waiting for the instance to no longer appear in the tenants instance list. - - Returns: - A boolean indicating that the instances have all been confirmed deleted. - - get_all_tenants() - Retrieves the list of all tenants from the keystone service - - This function uses ciao-cli to get a list of all possible tenants - from the keystone service. It is called using the admin context. - - Returns: - A list of dictionary representations of the tenants found - - get_all_workloads() - Retrieves the list of workload templates from the ciao cluster - - Returns: - A list of dictionary representations of the workloads found - - get_cnci() - Gets a list of all CNCIs on the ciao cluster. - - This function is called with the admin context. - - Returns: - A list of dictionary representations of a CNCI - - get_instances() - Retrieve all created instances for a tenant - - Returns: - A list of dictionary representations of an instance - - launch_all_workloads(num='1') - Attempt to create an instance for all possible workloads - - This function will get all the possible workloads, then - attempt to create an instance for each one. - - Args: - num: the number of instances per workload to create. Default is 1 - - Returns: - A boolean indicating whether the instances where successfully started - - launch_workload(uuid, num) - Attempt to start a number of instances of a specified workload type - - This function will call ciao-cli and tell it to create an instance - of a particular workload. - - Args: - uuid: The workload UUID to start - num: The number of instances of this workload to start - - Returns: - A boolean indicating whether the instances were successfully started - - main() - Start the BAT tests - - Confirm that the user has defined the environment variables we need, - and check for optional arguments. Start the unittests - output in - TAP format. - - Returns: - Error if ENV is not set - - start_random_workload(num='1') - Attempt to start a number of instances of a random workload - - This function will get all the possible workloads, then randomly - pick one to start. - - Args: - num: the number of instances to create. Default is 1 - - Returns: - A boolean indicating whether the instances where successfully started - - wait_till_running(uuid) - Wait in a loop till an instances status has changed to active - - This function will loop for retry_count number of times checking - the status of an Instance. If the status is not active, it - will sleep for one second and try again. - - Returns: - A boolean indicating whether the instance is active or not - -DATA - cli_timeout = 30 - retry_count = 60 diff --git a/_release/bat/README.md b/_release/bat/README.md new file mode 100644 index 000000000..2c6a28cb9 --- /dev/null +++ b/_release/bat/README.md @@ -0,0 +1,71 @@ +# BAT tests + +This folder contains a set of BAT tests. Each set of tests validates a specific +part of ciao, such as storage, and is implemented by a separate go package in one of +the following sub folders. + +``` +. +├── base - Basic tests that verify that the cluster is functional +``` + +The tests are implemented using the go testing framework. This is convenient +as this framework is used for ciao's unit tests and so is already familiar +to ciao developers, it requires no additional dependencies and it works with ciao's +existing test case runner, test-cases. + +# A Short Guide to Running the BAT Tests + +## Set up + +The BAT tests require that certain environment variables have been set before they +can be run: + +* "CIAO_IDENTITY" - the URL and port number of your identity service +* "CIAO_CONTROLLER" - the URL and port number of the ciao compute service +* "CIAO_USERNAME" - a test user with user level access to a test tenant +* "CIAO_PASSWORD" - your test user's password +* "CIAO_ADMIN_USERNAME" - your cluster admin user name +* "CIAO_ADMIN_PASSWORD" - your cluster admin password. + +## Running all the BAT tests + +``` +# cd $GOPATH/src/github.com/01org/ciao/_release/bat +# go test -v ./... +``` + +## Run the BAT Tests and Generate a Pretty Report + +``` +# cd $GOPATH/src/github.com/01org/ciao/_release/bat +# test-cases ./... +``` + +## Run the BAT Tests and Generate TAP report + +``` +# cd $GOPATH/src/github.com/01org/ciao/_release/bat +# test-cases -format tap ./... +``` + +## Run the BAT Tests and Generate a Test Plan + +``` +# cd $GOPATH/src/github.com/01org/ciao/_release/bat +# test-cases -format html ./... +``` + +## Run a Single Set of Tests + +``` +# cd $GOPATH/src/github.com/01org/ciao/_release/bat +# go test -v github.com/01org/ciao/_release/bat/base +``` + +## Run a Single Test + +``` +# cd $GOPATH/src/github.com/01org/ciao/_release/bat +# go test -v -run TestGetAllInstances ./... +``` diff --git a/_release/bat/base/base.go b/_release/bat/base/base.go new file mode 100644 index 000000000..b1795d28a --- /dev/null +++ b/_release/bat/base/base.go @@ -0,0 +1,18 @@ +// +// Copyright (c) 2016 Intel Corporation +// +// 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. +// + +// base is a placeholder package for the basic BAT tests. +package base diff --git a/_release/bat/base/base_test.go b/_release/bat/base/base_test.go new file mode 100644 index 000000000..e5ce05121 --- /dev/null +++ b/_release/bat/base/base_test.go @@ -0,0 +1,310 @@ +// +// Copyright (c) 2016 Intel Corporation +// +// 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. +// + +package base + +import ( + "context" + "flag" + "os" + "testing" + "time" + + "github.com/01org/ciao/bat" +) + +const standardTimeout = time.Second * 300 + +// Get all tenants +// +// TestGetTenants calls ciao-cli tenant list -all. +// +// The test passes if the list of tenants defined for the cluster can +// be retrieved, even if the list is empty. +func TestGetTenants(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + _, err := bat.GetAllTenants(ctx) + cancelFunc() + if err != nil { + t.Fatalf("Failed to retrieve tenant list : %v", err) + } +} + +// Confirm that the cluster is ready +// +// Retrieve the cluster status. +// +// Cluster status is retrived successfully, the cluster contains more than one +// node and all nodes are ready. +func TestGetClusterStatus(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + status, err := bat.GetClusterStatus(ctx) + cancelFunc() + if err != nil { + t.Fatalf("Failed to retrieve cluster status : %v", err) + } + + if status.TotalNodes == 0 { + t.Fatalf("Cluster has no nodes") + } + + if status.TotalNodes != status.TotalNodesReady { + t.Fatalf("Some nodes in the cluster are not ready") + } +} + +// Get all available workloads +// +// TestGetWorkloads calls ciao-cli workload list +// +// The test passes if the list of workloads defined for the cluster can +// be retrieved, even if the list is empty. +func TestGetWorkloads(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + _, err := bat.GetAllWorkloads(ctx, "") + cancelFunc() + if err != nil { + t.Fatalf("Failed to retrieve workload list : %v", err) + } +} + +// Start a random workload, then make sure it's listed +// +// Retrieves the list of workloads, selects a random workload, +// creates an instance of that workload and retrieves the instance's +// status. The instance is then deleted. +// +// The workload should be successfully launched, the status should +// be readable and the instance deleted. +func TestGetInstance(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + defer cancelFunc() + + instances, err := bat.StartRandomInstances(ctx, "", 1) + if err != nil { + t.Fatalf("Failed to launch instance: %v", err) + } + + _, err = bat.RetrieveInstanceStatus(ctx, "", instances[0]) + if err != nil { + t.Errorf("Failed to retrieve instance status: %v", err) + } + + scheduled, err := bat.WaitForInstancesLaunch(ctx, "", instances, false) + if err != nil { + t.Errorf("Instance %s did not launch: %v", instances[0], err) + } + + _, err = bat.DeleteInstances(ctx, "", scheduled) + if err != nil { + t.Errorf("Failed to delete instance %s: %v", instances[0], err) + } +} + +// Start one instance of all workloads +// +// Retrieve list of available workloads, start one instance of each workload and +// wait for that instance to start. Delete all started instances. +// +// The workload list should be retrieved correctly, the instances should be +// launched and achieve active status. All instances should be deleted +// successfully. +func TestStartAllWorkloads(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + defer cancelFunc() + + workloads, err := bat.GetAllWorkloads(ctx, "") + if err != nil { + t.Fatalf("Unable to retrieve workloads %v", err) + } + + instances := make([]string, 0, len(workloads)) + for _, wkl := range workloads { + launched, err := bat.LaunchInstances(ctx, "", wkl.ID, 1) + if err != nil { + t.Errorf("Unable to launch instance for workload %s : %v", + wkl.ID, err) + continue + } + scheduled, err := bat.WaitForInstancesLaunch(ctx, "", launched, true) + if err != nil { + t.Errorf("Instance %s did not launch correctly : %v", launched[0], err) + } + instances = append(instances, scheduled...) + } + + _, err = bat.DeleteInstances(ctx, "", instances) + if err != nil { + t.Errorf("Failed to delete instances: %v", err) + } +} + +// Start a random workload, then get CNCI information +// +// Start a random workload and verify that there is at least one CNCI present, and that +// a CNCI exists for the tenant of the instance that has just been created. +// +// The instance should be started correctly, at least one CNCI should be returned and +// we should have a CNCI servicing the tenant to which our instance belongs. +func TestGetCNCIs(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + defer cancelFunc() + + instances, err := bat.StartRandomInstances(ctx, "", 1) + if err != nil { + t.Fatalf("Failed to launch instance: %v", err) + } + + defer func() { + scheduled, err := bat.WaitForInstancesLaunch(ctx, "", instances, false) + if err != nil { + t.Errorf("Instance %s did not launch: %v", instances[0], err) + } + + _, err = bat.DeleteInstances(ctx, "", scheduled) + if err != nil { + t.Errorf("Failed to delete instances: %v", err) + } + }() + + CNCIs, err := bat.GetCNCIs(ctx) + if err != nil { + t.Fatalf("Failed to retrieve CNCIs: %v", err) + } + + if len(CNCIs) == 0 { + t.Fatalf("No CNCIs found") + } + + instanceDetails, err := bat.GetInstance(ctx, "", instances[0]) + if err != nil { + t.Fatalf("Unable to retrieve instance[%s] details: %v", + instances[0], err) + } + + foundTenant := false + for _, v := range CNCIs { + if v.TenantID == instanceDetails.TenantID { + foundTenant = true + break + } + } + + if !foundTenant { + t.Fatalf("Unable to locate a CNCI for instance[%s]", instances[0]) + } +} + +// Start 3 random instances and make sure they're all listed +// +// Start 3 random instances, wait for them to be scheduled and retrieve +// their details. Check some of the instance fields to ensure they're +// valid. Finally, delete the instances. +// +// Instances should be created and scheduled. Information about the +// instances should be sucessfully retrieved and this information should +// contain valid fields. +func TestGetAllInstances(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + defer cancelFunc() + + instances, err := bat.StartRandomInstances(ctx, "", 3) + if err != nil { + t.Fatalf("Failed to launch instance: %v", err) + } + + scheduled, err := bat.WaitForInstancesLaunch(ctx, "", instances, false) + defer func() { + _, err := bat.DeleteInstances(ctx, "", scheduled) + if err != nil { + t.Errorf("Failed to delete instances: %v", err) + } + }() + if err != nil { + t.Fatalf("Instance %s did not launch: %v", instances[0], err) + } + + instanceDetails, err := bat.GetAllInstances(ctx, "") + if err != nil { + t.Fatalf("Failed to retrieve instances: %v", err) + } + + for _, instance := range instances { + instanceDetail, ok := instanceDetails[instance] + if !ok { + t.Fatalf("Failed to retrieve instance %s", instance) + } + + // Check some basic information + + if instanceDetail.FlavorID == "" || instanceDetail.HostID == "" || + instanceDetail.TenantID == "" || instanceDetail.MacAddress == "" || + instanceDetail.PrivateIP == "" { + t.Fatalf("Instance missing information: %+v", instanceDetail) + } + } +} + +// Start a random workload, then delete it +// +// Start a random instance and wait for the instance to be scheduled. Delete all +// the instances in the current tenant and then retrieve the list of all instances. +// +// The instance should be started and scheduled, the DeleteAllInstances command should +// succeed and GetAllInstances command should return 0 instances. +func TestDeleteAllInstances(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + defer cancelFunc() + + instances, err := bat.StartRandomInstances(ctx, "", 1) + if err != nil { + t.Fatalf("Failed to launch instance: %v", err) + } + + _, err = bat.WaitForInstancesLaunch(ctx, "", instances, false) + if err != nil { + t.Errorf("Instance %s did not launch: %v", instances[0], err) + } + + err = bat.DeleteAllInstances(ctx, "") + if err != nil { + t.Fatalf("Failed to delete all instances: %v", err) + } + + instanceDetails, err := bat.GetAllInstances(ctx, "") + if err != nil { + t.Fatalf("Failed to retrieve instances: %v", err) + } + + if len(instanceDetails) != 0 { + t.Fatalf("0 instances expected. Found %d", len(instanceDetails)) + } +} + +// TestMain ensures that all instances have been deleted when the tests finish. +// The individual tests do try to clean up after themsevles but there's always +// the chance that a bug somewhere in ciao could lead to something not getting +// deleted. +func TestMain(m *testing.M) { + flag.Parse() + err := m.Run() + + ctx, cancelFunc := context.WithTimeout(context.Background(), standardTimeout) + _ = bat.DeleteAllInstances(ctx, "") + cancelFunc() + + os.Exit(err) +} diff --git a/_release/bat/release_test.py b/_release/bat/release_test.py deleted file mode 100644 index 9928c6f0d..000000000 --- a/_release/bat/release_test.py +++ /dev/null @@ -1,471 +0,0 @@ -# -# Copyright (c) 2016 Intel Corporation -# -# 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. -# - -""" Basic Acceptance Tests for the ciao project - -This module contains a set of tests that should be run on a ciao -cluster prior to submitting a pull request. The tests can be -run on a physical cluster, or on the ciao single VM setup. -The output is a TAP (Test Anything Protocol) format file, report.tap. -These tests utilize the python unittest framework. - -Prior to running the tests, the following environment variables -must be set: - "CIAO_IDENTITY" - the URL and port number of your identity service - "CIAO_CONTROLLER" - the URL and port number of the ciao compute service - "CIAO_USERNAME" - a test user with user level access to a test tenant - "CIAO_PASSWORD" - your test user's password - "CIAO_ADMIN_USERNAME" - your cluster admin user name - "CIAO_ADMIN_PASSWORD" - your cluster admin pass word. - -There are 2 configurable parameters that may be set: - - command_timeout - the length of time the test will wait for a - ciao-cli command to return. (default is 30 seconds) - cluster_timeout - the length of time to wait till an action has occurred - in the cluster (default is 60 seconds). - -""" -import subprocess32 -import os -import unittest -import tap -import time -import sys -import random -import argparse - -cli_timeout = 30 -retry_count = 60 - -def ciao_user_env(): - """Sets the user environment up for ciao-cli with the user role - - Copies the current environment and returns an environment - that ciao-cli will use for user role operations - - Returns: - an os env dict - - """ - return os.environ.copy() - -def ciao_admin_env(): - """Sets the user environment up for ciao-cli with the admin role - - Copies the current environment and returns an environment - that ciao-cli will use for admin role operations - - Returns: - an os env dict - - """ - ciao_env = os.environ.copy() - ciao_env["CIAO_USERNAME"] = ciao_env["CIAO_ADMIN_USERNAME"] - ciao_env["CIAO_PASSWORD"] = ciao_env["CIAO_ADMIN_PASSWORD"] - return ciao_env - -# implement a wait loop that waits for all instances to move to "active" -# but timeout after so many tries. -def wait_till_active(uuid): - """Wait in a loop till an instances status has changed to active - - This function will loop for retry_count number of times checking - the status of an Instance. If the status is not active, it - will sleep for one second and try again. - - Returns: - A boolean indicating whether the instance is active or not - - """ - count = retry_count - - while count > 0: - instances = get_instances() - for i in instances: - if i["uuid"] == uuid: - if i["status"] == "active": - return True - else: - break - time.sleep(1) - count -= 1 - - return False - -def launch_workload(uuid, num): - """Attempt to start a number of instances of a specified workload type - - This function will call ciao-cli and tell it to create an instance - of a particular workload. - - Args: - uuid: The workload UUID to start - num: The number of instances of this workload to start - - Returns: - A boolean indicating whether the instances were successfully started - """ - args = ['ciao-cli', 'instance', 'add', '-workload', uuid, '-instances', num] - - try: - output = subprocess32.check_output(args, env=ciao_user_env(), timeout=cli_timeout) - lines = output.splitlines() - line_iter = iter(lines) - - for line in line_iter: - uuid = line.split(":")[1].lstrip(' ').rstrip() - done = wait_till_active(uuid) - if not done: - return False - - return True - - except subprocess32.CalledProcessError as err: - print err.output - return False - -def start_random_workload(num="1"): - """Attempt to start a number of instances of a random workload - - This function will get all the possible workloads, then randomly - pick one to start. - - Args: - num: the number of instances to create. Default is 1 - - Returns: - A boolean indicating whether the instances where successfully started - """ - workloads = get_all_workloads() - if not workloads: - return False - - index = random.randint(0, len(workloads) - 1) - - return launch_workload(workloads[index]["uuid"], num) - -def launch_all_workloads(num="1"): - """Attempt to create an instance for all possible workloads - - This function will get all the possible workloads, then - attempt to create an instance for each one. - - Args: - num: the number of instances per workload to create. Default is 1 - - Returns: - A boolean indicating whether the instances where successfully started - """ - workloads = get_all_workloads() - if not workloads: - return False - - for workload in workloads: - # quit on first failure - success = launch_workload(workload["uuid"], num) - if not success: - return False - - return True - -def get_all_workloads(): - """Retrieves the list of workload templates from the ciao cluster - - Returns: - A list of dictionary representations of the workloads found - """ - args = ['ciao-cli', 'workload', 'list'] - - workloads = [] - - try: - output = subprocess32.check_output(args, env=ciao_user_env(), timeout=cli_timeout) - - except subprocess32.CalledProcessError as err: - print err.output - return workloads - - lines = output.splitlines() - line_iter = iter(lines) - - for line in line_iter: - if line.startswith("Workload"): - workload = { - "name": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "uuid": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "image_uuid": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "cpus": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "mem": next(line_iter).split(":")[1].lstrip(' ').rstrip() - } - - workloads.append(workload) - - return workloads - -def get_all_tenants(): - """Retrieves the list of all tenants from the keystone service - - This function uses ciao-cli to get a list of all possible tenants - from the keystone service. It is called using the admin context. - - Returns: - A list of dictionary representations of the tenants found - """ - args = ['ciao-cli', 'tenant', 'list', '-all'] - - tenants = [] - - try: - output = subprocess32.check_output(args, env=ciao_admin_env(), timeout=cli_timeout) - - except subprocess32.CalledProcessError as err: - print err.output - return tenants - - lines = output.splitlines() - line_iter = iter(lines) - - for line in line_iter: - if line.startswith("Tenant"): - uuid = next(line_iter).split(" ")[1] - name = next(line_iter).split(" ")[1] - tenant = { - "uuid": uuid, - "name": name - } - tenants.append(tenant) - - return tenants - -def check_cluster_status(): - """Confirms that the ciao cluster is fully operational - - This function uses ciao-cli to get the list of all compute/network nodes. - It confirms that the number of ready nodes is equal to the total number of nodes - It is called with the admin context. - - Returns: - A boolean indicating whether the cluster is ready or not. - """ - args = ['ciao-cli', 'node', 'status'] - - try: - output = subprocess32.check_output(args, env=ciao_admin_env(), timeout=cli_timeout) - - except subprocess32.CalledProcessError as err: - print err.output - return False - - lines = output.splitlines() - total = lines[0].split(" ")[2] - ready = lines[1].split(" ")[1] - - return total == ready - -def get_cnci(): - """Gets a list of all CNCIs on the ciao cluster. - - This function is called with the admin context. - - Returns: - A list of dictionary representations of a CNCI - """ - args = ['ciao-cli', 'node', 'list', '-cnci'] - - cncis = [] - - try: - output = subprocess32.check_output(args, env=ciao_admin_env(), timeout=cli_timeout) - - except subprocess32.CalledProcessError as err: - print err.output - return cncis - - lines = output.splitlines() - line_iter = iter(lines) - - for line in line_iter: - if line.startswith("CNCI"): - cnci = { - "uuid": next(line_iter).split(":")[1], - "tenant_uuid": next(line_iter).split(":")[1], - "ip": next(line_iter).split(":")[1] - } - cncis.append(cnci) - - return cncis - -def delete_all_instances(): - """Deletes all instances for a particular tenant - - This function uses ciao-cli to try to delete all previously created instances. - It then confirms that the instances were deleted by looping for retry_count - waiting for the instance to no longer appear in the tenants instance list. - - Returns: - A boolean indicating that the instances have all been confirmed deleted. - """ - args = ['ciao-cli', 'instance', 'delete', '-all'] - - try: - output = subprocess32.check_output(args, env=ciao_user_env(), timeout=cli_timeout) - - except subprocess32.CalledProcessError as err: - print err.output - return False - - if output.startswith("os-delete"): - count = retry_count - - while count > 0: - if len(get_instances()) == 0: - return True - - time.sleep(1) - count -= 1 - - return False - - return False - -def get_instances(): - """Retrieve all created instances for a tenant - - Returns: - A list of dictionary representations of an instance - """ - args = ['ciao-cli', 'instance', 'list', '-detail'] - - instances = [] - - myenv = ciao_user_env() - - try: - output = subprocess32.check_output(args, env=myenv, timeout=cli_timeout) - - except subprocess32.CalledProcessError as err: - print err.output - return instances - - lines = output.splitlines() - line_iter = iter(lines) - - for line in line_iter: - if line.startswith("Instance"): - instance = { - "uuid": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "status": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "ip": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "mac": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "cn_uuid": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "image_uuid": next(line_iter).split(":")[1].lstrip(' ').rstrip(), - "tenant_uuid": next(line_iter).split(":")[1].lstrip(' ').rstrip() - } - - instances.append(instance) - - return instances - -class BATTests(unittest.TestCase): - """Basic Acceptance Tests for the ciao project""" - def setUp(self): - pass - - def tearDown(self): - delete_all_instances() - time.sleep(2) - - def test_get_tenants(self): - """Get all tenants""" - self.failUnless(get_all_tenants()) - - def test_cluster_status(self): - """Confirm that the cluster is ready""" - self.failUnless(check_cluster_status()) - - def test_get_workloads(self): - """Get all available workloads""" - self.failUnless(get_all_workloads()) - - def test_start_all_workloads(self): - """Start one instance of all workloads""" - self.failUnless(launch_all_workloads()) - - def test_get_cncis(self): - """Start a random workload, then get CNCI information""" - self.failUnless(start_random_workload()) - self.failUnless(get_cnci()) - - def test_get_instances(self): - """Start a random workload, then make sure it's listed""" - self.failUnless(start_random_workload()) - time.sleep(5) - instances = get_instances() - self.failUnless(len(instances) == 1) - - def test_delete_all_instances(self): - """Start a random workload, then delete it""" - self.failUnless(start_random_workload()) - self.failUnless(delete_all_instances()) - self.failUnless(not get_instances()) - - -def main(): - """Start the BAT tests - - Confirm that the user has defined the environment variables we need, - and check for optional arguments. Start the unittests - output in - TAP format. - - Returns: - Error if ENV is not set - """ - global cli_timeout - global retry_count - - envvars = [ - "CIAO_IDENTITY", - "CIAO_CONTROLLER", - "CIAO_USERNAME", - "CIAO_PASSWORD", - "CIAO_ADMIN_USERNAME", - "CIAO_ADMIN_PASSWORD" - ] - - for var in envvars: - if var not in os.environ: - err = "env var %s not set" % var - sys.exit(err) - - parser = argparse.ArgumentParser(description="ciao Basic Acceptance Tests") - parser.add_argument("--command_timeout", action="store", dest="cli_timeout", - help="Seconds to wait for a command to complete", - default=300) - parser.add_argument("--cluster_timeout", action="store", dest="retry_count", - help="Seconds to wait for cluster to respond", - default=60) - - args = parser.parse_args() - - cli_timeout = args.cli_timeout - retry_count = args.retry_count - - outfile = open("./report.tap", "w") - unittest.main(testRunner=tap.TAPTestRunner(stream=outfile)) - -if __name__ == '__main__': - main() diff --git a/_release/bat/requirements.txt b/_release/bat/requirements.txt deleted file mode 100644 index 6e4c50a79..000000000 --- a/_release/bat/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -linecache2==1.0.0 -six==1.10.0 -subprocess32==3.2.7 -tap.py==2.0 -traceback2==1.4.0 -unittest2==1.1.0 diff --git a/bat/README.md b/bat/README.md new file mode 100644 index 000000000..591011457 --- /dev/null +++ b/bat/README.md @@ -0,0 +1,32 @@ +BAT +====== + +The BAT package provides a set of utility functions that can be used +to test and manipulate a ciao cluster. These functions are just wrappers +around the ciao-cli command. They invoke ciao-cli commands and parse +and return the output from these commands in easily consumable go +values. Invoking the ciao-cli commands directly, rather than calling +the REST APIs exposed by ciao's various services, allows us to test +a little bit more of ciao. + +Example +--------- + +Here's a quick example. The following code retrieves the instances defined +on the default tenant and prints out their UUIDs and statuses. + +``` + instances, err := bat.GetAllInstances(context.Background(), "") + if err != nil { + return err + } + for uuid, instance := range instances { + fmt.Printf("%s : %s\n", uuid, instance.Status) + } + +``` + +The bat.GetAllInstances command calls ciao-cli instance list. + + + diff --git a/bat/bat.go b/bat/bat.go new file mode 100644 index 000000000..d4ba5847b --- /dev/null +++ b/bat/bat.go @@ -0,0 +1,553 @@ +// +// Copyright (c) 2016 Intel Corporation +// +// 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. +// + +// Package bat contains a number of helper functions that can be used to perform +// various operations on a ciao cluster such as creating an instance or retrieving +// a list of all the defined workloads, etc. All of these helper functions are +// implemented by calling ciao-cli rather than by using ciao's REST APIs. This +// package is mainly intended for use by BAT tests. Manipulating the cluster +// via ciao-cli, rather than through the REST APIs, allows us to test a little +// bit more of ciao. +package bat + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "math/rand" + "os" + "os/exec" + "strings" + "time" +) + +const instanceTemplateDesc = `{ "host_id" : "{{.HostID | js }}", + "tenant_id" : "{{.TenantID | js }}", "flavor_id" : "{{.Flavor.ID | js}}", + "image_id" : "{{.Image.ID | js}}", "status" : "{{.Status | js}}", + "ssh_ip" : "{{.SSHIP | js }}", "ssh_port" : {{.SSHPort}} + {{ $addrLen := len .Addresses.Private }} + {{- if gt $addrLen 0 }} + {{- with index .Addresses.Private 0 -}} + , "private_ip" : "{{.Addr | js }}", "mac_address" : "{{.OSEXTIPSMACMacAddr | js -}}" + {{end -}} + {{- end }} + } +` + +// Tenant contains basic information about a tenant +type Tenant struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Workload contains detailed information about a workload +type Workload struct { + ID string `json:"id"` + Name string `json:"name"` + ImageUUID string `json:"image_uuid"` + CPUs int `json:"cpus"` + Mem int `json:"mem"` +} + +// Instance contains detailed information about an instance +type Instance struct { + HostID string `json:"host_id"` + TenantID string `json:"tenant_id"` + FlavorID string `json:"flavor_id"` + ImageID string `json:"image_id"` + Status string `json:"status"` + PrivateIP string `json:"private_ip"` + MacAddress string `json:"mac_address"` + SSHIP string `json:"ssh_ip"` + SSHPort int `json:"ssh_port"` +} + +// CNCI contains information about a CNCI +type CNCI struct { + TenantID string `json:"tenant_id"` + IPv4 string `json:"ip"` + Geography string `json:"geo"` + Subnets []string `json:"subnets"` +} + +// ClusterStatus contains information about the status of a ciao cluster +type ClusterStatus struct { + TotalNodes int `json:"nodes"` + TotalNodesReady int `json:"ready"` + TotalNodesFull int `json:"full"` + TotalNodesOffline int `json:"offline"` + TotalNodesMaintenance int `json:"maintenance"` +} + +func checkEnv(vars []string) error { + for _, k := range vars { + if os.Getenv(k) == "" { + return fmt.Errorf("%s is not defined", k) + } + } + return nil +} + +// RunCIAOCLI execs the ciao-cli command with a set of arguments. The ciao-cli +// process will be killed if the context is Done. An error will be returned if +// the following environment are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_USERNAME, CIAO_PASSWORD. On success the data written to ciao-cli on stdout +// will be returned. +func RunCIAOCLI(ctx context.Context, tenant string, args []string) ([]byte, error) { + vars := []string{"CIAO_IDENTITY", "CIAO_CONTROLLER", "CIAO_USERNAME", "CIAO_PASSWORD"} + if err := checkEnv(vars); err != nil { + return nil, err + } + + if tenant != "" { + args = append([]string{"-tenant", tenant}, args...) + } + + data, err := exec.CommandContext(ctx, "ciao-cli", args...).Output() + if err != nil { + var failureText string + if err, ok := err.(*exec.ExitError); ok { + failureText = string(err.Stderr) + } + return nil, fmt.Errorf("failed to launch ciao-cli %v : %v\n%s", + args, err, failureText) + } + + return data, nil +} + +// RunCIAOCLIJS is similar to RunCIAOCLI with the exception that the output +// of the ciao-cli command is expected to be in json format. The json is +// decoded into the jsdata parameter which should be a pointer to a type +// that corresponds to the json output. +func RunCIAOCLIJS(ctx context.Context, tenant string, args []string, jsdata interface{}) error { + data, err := RunCIAOCLI(ctx, tenant, args) + if err != nil { + return err + } + + err = json.Unmarshal(data, jsdata) + if err != nil { + return err + } + + return nil +} + +// RunCIAOCLIAsAdmin execs the ciao-cli command as the admin user with a set of +// provided arguments. The ciao-cli process will be killed if the context is +// Done. An error will be returned if the following environment are not set; +// CIAO_IDENTITY, CIAO_CONTROLLER, CIAO_ADMIN_USERNAME, CIAO_ADMIN_PASSWORD. +// On success the data written to ciao-cli on stdout will be returned. +func RunCIAOCLIAsAdmin(ctx context.Context, tenant string, args []string) ([]byte, error) { + vars := []string{"CIAO_IDENTITY", "CIAO_CONTROLLER", "CIAO_ADMIN_USERNAME", "CIAO_ADMIN_PASSWORD"} + if err := checkEnv(vars); err != nil { + return nil, err + } + + if tenant != "" { + args = append([]string{"-tenant", tenant}, args...) + } + + env := os.Environ() + envCopy := make([]string, 0, len(env)) + for _, v := range env { + if !strings.HasPrefix(v, "CIAO_USERNAME=") && + !strings.HasPrefix(v, "CIAO_PASSWORD=") { + envCopy = append(envCopy, v) + } + } + envCopy = append(envCopy, fmt.Sprintf("CIAO_USERNAME=%s", + os.Getenv("CIAO_ADMIN_USERNAME"))) + envCopy = append(envCopy, fmt.Sprintf("CIAO_PASSWORD=%s", + os.Getenv("CIAO_ADMIN_PASSWORD"))) + + cmd := exec.CommandContext(ctx, "ciao-cli", args...) + cmd.Env = envCopy + data, err := cmd.Output() + if err != nil { + var failureText string + if err, ok := err.(*exec.ExitError); ok { + failureText = string(err.Stderr) + } + return nil, fmt.Errorf("failed to launch ciao-cli %v : %v\n%v", + args, err, failureText) + } + + return data, nil +} + +// RunCIAOCLIAsAdminJS is similar to RunCIAOCLIAsAdmin with the exception that +// the output of the ciao-cli command is expected to be in json format. The +// json is decoded into the jsdata parameter which should be a pointer to a type +// that corresponds to the json output. +func RunCIAOCLIAsAdminJS(ctx context.Context, tenant string, args []string, + jsdata interface{}) error { + data, err := RunCIAOCLIAsAdmin(ctx, tenant, args) + if err != nil { + return err + } + + err = json.Unmarshal(data, jsdata) + if err != nil { + return err + } + + return nil +} + +// GetAllTenants retrieves a list of all tenants in the cluster by calling +// ciao-cli tenant list -all. An error will be returned if the following +// environment variables are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_ADMIN_USERNAME, CIAO_ADMIN_PASSWORD. +func GetAllTenants(ctx context.Context) ([]*Tenant, error) { + var tenants []*Tenant + template := ` +[ +{{- range $i, $val := .}} + {{- if $i }},{{end}} + { "id" : "{{$val.ID | js }}", "name" : "{{$val.Name | js }}" } +{{- end }} +] +` + args := []string{"tenant", "list", "-all", "-f", template} + err := RunCIAOCLIAsAdminJS(ctx, "", args, &tenants) + if err != nil { + return nil, err + } + + return tenants, nil +} + +// GetAllWorkloads retrieves a list of all workloads in the cluster by calling +// ciao-cli workload list. An error will be returned if the following +// environment variables are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_USERNAME, CIAO_PASSWORD. +func GetAllWorkloads(ctx context.Context, tenant string) ([]Workload, error) { + var workloads []Workload + template := ` +[ +{{- range $i, $val := .}} + {{- if $i }},{{end}} + { "id" : "{{$val.ID | js }}", "name" : "{{$val.Name | js }}", + "image_uuid" : "{{$val.Disk | js }}", "cpus" : {{$val.Vcpus}}, + "mem" : {{$val.RAM}} } +{{- end }} +] +` + args := []string{"workload", "list", "-f", template} + err := RunCIAOCLIJS(ctx, tenant, args, &workloads) + if err != nil { + return nil, err + } + + return workloads, nil +} + +// GetInstance returns an Instance structure that contains information +// about a specific instance. The informaion is retrieved by calling +// ciao-cli show --instance. An error will be returned if the following +// environment variables are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_USERNAME, CIAO_PASSWORD. +func GetInstance(ctx context.Context, tenant string, uuid string) (*Instance, error) { + var instance *Instance + args := []string{"instance", "show", "--instance", uuid, "-f", instanceTemplateDesc} + err := RunCIAOCLIJS(ctx, tenant, args, &instance) + if err != nil { + return nil, err + } + + return instance, nil +} + +// GetAllInstances returns information about all instances in the specified +// tenant in a map. The key of the map is the instance uuid. The information +// is retrieved by calling ciao-cli instance list. An error will be returned +// if the following environment variables are not set; CIAO_IDENTITY, +// CIAO_CONTROLLER, CIAO_USERNAME, CIAO_PASSWORD. +func GetAllInstances(ctx context.Context, tenant string) (map[string]*Instance, error) { + var instances map[string]*Instance + template := ` +{ +{{- range $i, $val := .}} + {{- if $i }},{{end}} + "{{$val.ID | js }}" : {{with $val}}` + instanceTemplateDesc + `{{end}} +{{- end }} +} +` + args := []string{"instance", "list", "-f", template} + err := RunCIAOCLIJS(ctx, tenant, args, &instances) + if err != nil { + return nil, err + } + + return instances, nil +} + +// RetrieveInstanceStatus retrieve the status of a specific instance. This +// information is retrieved using ciao-cli instance show. An error will be +// returned if the following environment variables are not set; CIAO_IDENTITY, +// CIAO_CONTROLLER, CIAO_USERNAME, CIAO_PASSWORD. +func RetrieveInstanceStatus(ctx context.Context, tenant string, instance string) (string, error) { + args := []string{"instance", "show", "-instance", instance, "-f", "{{.Status}}"} + data, err := RunCIAOCLI(ctx, tenant, args) + if err != nil { + return "", err + } + return string(data), nil +} + +// RetrieveInstancesStatuses retrieves the statuses of a slice of specific instances. +// This information is retrieved using ciao-cli instance list. An error will be +// returned if the following environment variables are not set; CIAO_IDENTITY, +// CIAO_CONTROLLER, CIAO_USERNAME, CIAO_PASSWORD. +func RetrieveInstancesStatuses(ctx context.Context, tenant string) (map[string]string, error) { + var statuses map[string]string + template := ` +{ +{{- range $i, $val := .}} + {{- if $i }},{{end}} + "{{$val.ID | js }}" : "{{$val.Status | js }}" +{{- end }} +} +` + args := []string{"instance", "list", "-f", template} + err := RunCIAOCLIJS(ctx, tenant, args, &statuses) + if err != nil { + return nil, err + } + return statuses, nil +} + +// DeleteInstance deletes a specific instance from the cluster. It deletes +// the instance using ciao-cli instance delete. An error will be returned +// if the following environment variables are not set; CIAO_IDENTITY, +// CIAO_CONTROLLER, CIAO_USERNAME, CIAO_PASSWORD. +func DeleteInstance(ctx context.Context, tenant string, instance string) error { + args := []string{"instance", "delete", "-instance", instance} + _, err := RunCIAOCLI(ctx, tenant, args) + return err +} + +// DeleteInstances deletes a set of instances provided by the instances slice. +// If the function encounters an error deleting an instance it records the error +// and proceeds to the delete the next instance. The function returns two values, +// an error and a slice of errors. A single error value is set if any of the +// instance deletion attempts failed. A slice of errors is also returned so that +// the caller can determine which of the deletion attempts failed. The indices +// in the error slice match the indicies in the instances slice, i.e., a non nil +// value in the first element of the error slice indicates that there was an +// error deleting the first instance in the instances slice. An error will be +// returned if the following environment variables are not set; CIAO_IDENTITY, +// CIAO_CONTROLLER, CIAO_USERNAME, CIAO_PASSWORD. +func DeleteInstances(ctx context.Context, tenant string, instances []string) ([]error, error) { + var err error + errs := make([]error, len(instances)) + + for i, instance := range instances { + errs[i] = DeleteInstance(ctx, tenant, instance) + if err == nil && errs[i] != nil { + err = fmt.Errorf("At least one instance deletion attempt failed") + } + } + + return errs, err +} + +// DeleteAllInstances deletes all the instances created for the specified +// tenant by calling ciao-cli instance delete -all. It returns an error +// if the ciao-cli command fails. An error will be returned if the following +// environment variables are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_USERNAME, CIAO_PASSWORD. +func DeleteAllInstances(ctx context.Context, tenant string) error { + args := []string{"instance", "delete", "-all"} + _, err := RunCIAOCLI(ctx, tenant, args) + return err +} + +func checkStatuses(instances []string, statuses map[string]string, + mustBeActive bool) ([]string, bool, error) { + + var err error + scheduled := make([]string, 0, len(instances)) + finished := true + for _, instance := range instances { + status, ok := statuses[instance] + if !ok { + if err == nil { + err = fmt.Errorf("Instance %s does not exist", instance) + } + continue + } + + scheduled = append(scheduled, instance) + + if status == "pending" { + finished = false + } else if err == nil && mustBeActive && status == "exited" { + err = fmt.Errorf("Instance %s has exited", instance) + } + } + + return scheduled, finished, err +} + +// WaitForInstancesLaunch waits for a slice of newly created instances to be +// scheduled. An instance is scheduled when its status changes from pending +// to exited or active. If mustBeActive is set to true, the function will +// fail if it sees an instance that has been scheduled but whose status is +// exited. The function returns a slice of instance UUIDs and an error. +// In the case of success, the returned slice of UUIDs will equal the instances +// array. In the case of error, these two slices may be different. This +// can happen if one or more of the instances has failed to launch. If errors +// are detected with multiple instances, e.g., mustBeActive is true and two +// instances have a status of 'exited' the error returned will refers to the +// first instance only. An error will be returned if the following +// environment variables are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_USERNAME, CIAO_PASSWORD. +func WaitForInstancesLaunch(ctx context.Context, tenant string, instances []string, + mustBeActive bool) ([]string, error) { + + scheduled := make([]string, 0, len(instances)) + for { + statuses, err := RetrieveInstancesStatuses(ctx, tenant) + if err != nil { + return scheduled, err + } + + var finished bool + scheduled, finished, err = checkStatuses(instances, statuses, mustBeActive) + if finished || err != nil { + return scheduled, err + } + + select { + case <-time.After(time.Second): + case <-ctx.Done(): + return scheduled, ctx.Err() + } + } +} + +func parseInstances(data []byte, num int) ([]string, error) { + instances := make([]string, num) + scanner := bufio.NewScanner(bytes.NewBuffer(data)) + for i := 0; i < num; i++ { + if !scanner.Scan() { + return nil, fmt.Errorf( + "Missing instance UUID. Found %d, expected %d", i, num) + } + + line := scanner.Bytes() + colonIndex := bytes.LastIndexByte(line, ':') + if colonIndex == -1 || colonIndex+2 >= len(line) { + return nil, fmt.Errorf("Unable to determine UUID of new instance") + } + instances[i] = string(bytes.TrimSpace(line[colonIndex+2:])) + } + + return instances, nil +} + +// LaunchInstances launches num instances of the specified workload. On success +// the function returns a slice of UUIDs of the new instances. The instances +// are launched using ciao-cli instance add. An error will be returned if the +// following environment variables are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_USERNAME, CIAO_PASSWORD. +func LaunchInstances(ctx context.Context, tenant string, workload string, num int) ([]string, error) { + args := []string{"instance", "add", "--workload", workload, + "--instances", fmt.Sprintf("%d", num)} + data, err := RunCIAOCLI(ctx, tenant, args) + if err != nil { + return nil, err + } + + return parseInstances(data, num) +} + +// StartRandomInstances starts a specified number of instances using +// a random workload. The UUIDs of the started instances are returned +// to the user. An error will be returned if the following +// environment variables are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_USERNAME, CIAO_PASSWORD. +func StartRandomInstances(ctx context.Context, tenant string, num int) ([]string, error) { + wklds, err := GetAllWorkloads(ctx, tenant) + if err != nil { + return nil, err + } + + if len(wklds) == 0 { + return nil, fmt.Errorf("No workloads defined") + } + + wkldUUID := wklds[rand.Intn(len(wklds))].ID + return LaunchInstances(ctx, tenant, wkldUUID, num) +} + +// GetCNCIs returns a map of the CNCIs present in the cluster. The key +// of the map is the CNCI ID. The CNCI information is retrieved using +// ciao-cli list -cnci command. An error will be returned if the +// following environment are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_ADMIN_USERNAME, CIAO_ADMIN_PASSWORD. +func GetCNCIs(ctx context.Context) (map[string]*CNCI, error) { + var CNCIs map[string]*CNCI + template := ` +{ +{{- range $i, $val := .}} + {{- if $i }},{{end}} + "{{$val.ID | js }}" : { + "tenant_id" : "{{$val.TenantID | js }}", "ip" : "{{$val.IPv4 | js}}", + "geo": "{{$val.Geography | js }}", "subnets": [ + {{- range $j, $net := $val.Subnets -}} + {{- if $j }},{{end -}} + "{{- $net.Subnet -}}" + {{- end -}} + ]} + {{- end }} +} +` + args := []string{"node", "list", "-cnci", "-f", template} + err := RunCIAOCLIAsAdminJS(ctx, "", args, &CNCIs) + if err != nil { + return nil, err + } + + return CNCIs, nil +} + +// GetClusterStatus returns the status of the ciao cluster. The information +// is retrieved by calling ciao-cli node status. An error will be returned +// if the following environment are not set; CIAO_IDENTITY, CIAO_CONTROLLER, +// CIAO_ADMIN_USERNAME, CIAO_ADMIN_PASSWORD. +func GetClusterStatus(ctx context.Context) (*ClusterStatus, error) { + var cs *ClusterStatus + template := ` +{ + "nodes" : {{.TotalNodes}}, "ready" : {{.TotalNodesReady}}, + "full" : {{.TotalNodesFull}}, "offline" : {{.TotalNodesOffline}}, + "maintenance" : {{.TotalNodesMaintenance}} +} +` + args := []string{"node", "status", "-f", template} + err := RunCIAOCLIAsAdminJS(ctx, "", args, &cs) + if err != nil { + return nil, err + } + + return cs, nil +} diff --git a/bat/bat_test.go b/bat/bat_test.go new file mode 100644 index 000000000..d52734664 --- /dev/null +++ b/bat/bat_test.go @@ -0,0 +1,125 @@ +// +// Copyright (c) 2016 Intel Corporation +// +// 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. +// + +package bat + +import "testing" + +var instances = []string{ + "d258443c-72c7-4971-8c2b-cb9925522c3e", + "64a0cca9-85a2-4733-988b-b4fe9a72dd0e", +} + +func TestGoodCheckStatuses(t *testing.T) { + goodStatus := map[string]string{ + "d258443c-72c7-4971-8c2b-cb9925522c3e": "active", + "64a0cca9-85a2-4733-988b-b4fe9a72dd0e": "active", + } + + scheduled, finished, err := checkStatuses(instances, goodStatus, true) + if len(scheduled) != 2 || !finished || err != nil { + t.Errorf("goodStatus check failed") + } +} + +func TestPendingCheckStatuses(t *testing.T) { + pendingStatus := map[string]string{ + "d258443c-72c7-4971-8c2b-cb9925522c3e": "pending", + "64a0cca9-85a2-4733-988b-b4fe9a72dd0e": "pending", + } + + scheduled, finished, err := checkStatuses(instances, pendingStatus, true) + if len(scheduled) != 2 || finished || err != nil { + t.Errorf("pendingStatus check failed") + } + + partialPendingStatus := map[string]string{ + "d258443c-72c7-4971-8c2b-cb9925522c3e": "active", + "64a0cca9-85a2-4733-988b-b4fe9a72dd0e": "pending", + } + + scheduled, finished, err = checkStatuses(instances, partialPendingStatus, true) + if len(scheduled) != 2 || finished || err != nil { + t.Errorf("pendingStatus check failed") + } +} + +func TestExitedCheckStatuses(t *testing.T) { + exitedStatus := map[string]string{ + "d258443c-72c7-4971-8c2b-cb9925522c3e": "active", + "64a0cca9-85a2-4733-988b-b4fe9a72dd0e": "exited", + } + + scheduled, finished, err := checkStatuses(instances, exitedStatus, true) + if len(scheduled) != 2 || !finished || err == nil { + t.Errorf("pendingStatus mustActive=true check failed") + } + + scheduled, finished, err = checkStatuses(instances, exitedStatus, false) + if len(scheduled) != 2 || !finished || err != nil { + t.Errorf("pendingStatus mustActive=false check failed") + } +} + +func TestMissingCheckStatuses(t *testing.T) { + missingStatus := map[string]string{ + "d258443c-72c7-4971-8c2b-cb9925522c3e": "active", + } + + scheduled, finished, err := checkStatuses(instances, missingStatus, false) + if len(scheduled) != 1 || !finished || err == nil { + t.Errorf("pendingStatus mustActive=false check failed") + } +} + +func TestParseInstances(t *testing.T) { + const goodOutput = `Created new instance: 50853f43-e308-4bbd-b75a-c2305bc40615 +Created new instance: c4a492dd-0df1-47e4-9407-a19c8e1820ee +Created new instance: f7709d71-8a1e-4295-8940-b32a5c82ede4 +` + instances, err := parseInstances([]byte(goodOutput), 3) + if err != nil || len(instances) != 3 || + instances[0] != "50853f43-e308-4bbd-b75a-c2305bc40615" || + instances[1] != "c4a492dd-0df1-47e4-9407-a19c8e1820ee" || + instances[2] != "f7709d71-8a1e-4295-8940-b32a5c82ede4" { + t.Errorf("parseInstance failed to parse positive test case") + } + + const missingColon = `Created new instance: 50853f43-e308-4bbd-b75a-c2305bc40615 +Created new instance c4a492dd-0df1-47e4-9407-a19c8e1820ee +Created new instance: f7709d71-8a1e-4295-8940-b32a5c82ede4 +` + instances, err = parseInstances([]byte(missingColon), 3) + if err == nil || instances != nil { + t.Errorf("parseInstance failed to parse missing colon error case") + } + + const extraNewline = `Created new instance: 50853f43-e308-4bbd-b75a-c2305bc40615 +Created new instance c4a492dd-0df1-47e4-9407-a19c8e1820ee + +Created new instance: f7709d71-8a1e-4295-8940-b32a5c82ede4 +` + + instances, err = parseInstances([]byte(extraNewline), 3) + if err == nil || instances != nil { + t.Errorf("parseInstance failed to parse extra newline error case") + } + + instances, err = parseInstances([]byte(goodOutput), 4) + if err == nil || instances != nil { + t.Errorf("parseInstance failed on too few instances error case") + } +} diff --git a/ciao-cli/node.go b/ciao-cli/node.go index c5a74a474..9b8925d86 100644 --- a/ciao-cli/node.go +++ b/ciao-cli/node.go @@ -186,7 +186,8 @@ func listCNCINodes(t *template.Template) error { } type nodeStatusCommand struct { - Flag flag.FlagSet + Flag flag.FlagSet + template string } func (cmd *nodeStatusCommand) usage(...string) { @@ -194,10 +195,24 @@ func (cmd *nodeStatusCommand) usage(...string) { Show cluster status `) + cmd.Flag.PrintDefaults() + fmt.Fprintf(os.Stderr, ` +The template passed to the -f option operates on a + +struct { + TotalNodes int + TotalNodesReady int + TotalNodesFull int + TotalNodesOffline int + TotalNodesMaintenance int +} +`) + os.Exit(2) } func (cmd *nodeStatusCommand) parseArgs(args []string) []string { + cmd.Flag.StringVar(&cmd.template, "f", "", "Template used to format output") cmd.Flag.Usage = func() { cmd.usage() } cmd.Flag.Parse(args) return cmd.Flag.Args() @@ -217,6 +232,11 @@ func (cmd *nodeStatusCommand) run(args []string) error { fatalf(err.Error()) } + if cmd.template != "" { + return outputToTemplate("node-status", cmd.template, + &status.Status) + } + fmt.Printf("Total Nodes %d\n", status.Status.TotalNodes) fmt.Printf("\tReady %d\n", status.Status.TotalNodesReady) fmt.Printf("\tFull %d\n", status.Status.TotalNodesFull)